31498 - [SC - High] Alchemix ALCX rewards are currently subject to...
Submitted on May 20th 2024 at 16:15:19 UTC by @Norah for Boost | Alchemix
Report ID: #31498
Report type: Smart Contract
Report severity: High
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
Upon the conclusion of the epoch/activePeriod, emissions are allocated to various components within the Alchemix system, including the
voter contract.These emissions are subsequently distributed to the respective
gaugesbased on the voting outcomes from the preceding epoch.The
distribute()function of the voter contract can be invoked by anyone, triggering the minter to dispatch the emissions to the voter contract and other destinations, while also updating the Index to reflect the reward per voting weight, accounting for the newly injected rewards.
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]]);
}
}
IMinter(minter).updatePeriod();
}Following this,
distribute()invokes the internal_distribute()function for each gauge within the voting contracts, updating theclaimable[gauge]amount in accordance with the newly updatedIndexvia internal callupdateFor()call.Lastly, the voter contract calls
notifyRewardAmount()on the gauge contract to dispatch the rewards based on theclaimable[gauge]amount.
Vulnerability Details
The vulnerability arises due to the caching of the
claimable[gauge]value before the execution of the_updateFor(_gauge)function.Consequently, the
IBaseGauge(_gauge).notifyRewardAmount(_claimable)call passes aclaimable[gauge]value that doesn't incorporate emissions from the previous epoch.As a result, the latest emissions are not transferred to the respective gauge but are only updated in the
claimable[gauge]mapping.Only way to invoke
IBaseGauge(_gauge).notifyRewardAmount(_claimable)is viadistribute()routine, which can only be called once a epoch after epoch period ends.
Impact Details
This leads to rewards being inaccessible to any gauge and consequently users for at least two weeks, until the next invocation of these routines after the two-week epoch duration.
Although these emission rewards for gauges are not entirely lost, as they are updated in the claimable[gauge] mapping, they become temporarily unattainable for a two-week period due to the delay in the distribute() routine.
References
Add any relevant links to documentation or code
Recommendation
Change the code of internal
_distribute()function so that_updateFor()is called before the caching theclaimable[gauge]to be used as parameter for the callIBaseGauge(_gauge).notifyRewardAmount(_claimable).
Proof of Concept
I've created a test to demonstrate that rewards for epoch
Xare only transferred during epochX+1, meaning after two weeks.Also, run this test after implementing the recommendations for better understanding as mentioned in POC.
Note: The rewards for the first epochs are zero due to a different bug (refer to report #31494).
I have attached output of the test in both the scenarios.
Add the following test into the voting.t.sol file of the test suite and execute it using the following command:
forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/{Alchemy-api-key} --match-test "testEmissionRewardsDistributionDelay" -vvv
Last updated
Was this helpful?