GithubHelp home page GithubHelp logo

2023-08-arbitrum-findings's People

Contributors

code423n4 avatar itsmetechjay avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

2023-08-arbitrum-findings's Issues

Lack of Quorum and Majority Enforcement in Security Council Member Removal

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L1-L300

Vulnerability details

The provided contract lacks the necessary enforcement mechanisms to ensure compliance with the Arbitrum DAO constitution's requirements for removing security council members. Specifically, it doesn't ensure the minimum quorum and majority vote conditions are met during the removal process.

Impact

The absence of quorum and majority enforcement in the security council member removal process can undermine the democratic and fair governance of the Arbitrum DAO. It creates a situation where decisions can be made without the required level of support, potentially leading to contentious and disputed outcomes. This vulnerability could also introduce a risk of centralization or manipulation of the security council.

Proof of Concept

The contract implements a mechanism for voting on the removal of security council members. However, it does not contain the required checks to ensure that the removal process adheres to the constitution's conditions. According to the constitution, security council members can only be removed if either of the following conditions are met:

  1. At least 10% of all Votable Tokens have cast votes "in favor" of removal, and at least 5/6 (83.33%) of all cast votes are "in favor" of removal.
  2. At least 9 of the Security Council members vote in favor of removal.
    The contract code doesn't include the necessary logic to enforce these conditions. As a result, it is possible for security council members to be removed without meeting the required quorum or majority vote, which could lead to decisions being made without sufficient consensus

Tools Used

Manual

Recommended Mitigation Steps

To address this issue, the contract should be updated to include explicit checks that enforce the constitution's quorum and majority vote requirements for the removal of security council members. This can be achieved by adding conditional statements that verify the number of votes in favor of removal and comparing them to the constitution's thresholds. Only if these conditions are met should the removal proceed.

Assessed type

Invalid Validation

Potential Precision Loss in Quorum Calculation of `ArbitrumGovernorVotesQuorumFractionUpgradeable` Contract

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/ArbitrumGovernorVotesQuorumFractionUpgradeable.sol#L37-L41

Vulnerability details

The ArbitrumGovernorVotesQuorumFractionUpgradeable contract has a potential issue related to precision loss in the quorum calculation process. This issue arises due to integer division being used in the quorum calculation formula. While the contract attempts to mitigate this by using a denominator value of 10,000 (10k) for the division, there is still a possibility of rounding or truncation of decimal values in the result.

Impact

The potential precision loss in the quorum calculation could impact the accuracy of the quorum value used for governance decisions. If the precision loss is significant, it might result in unexpected or inaccurate quorum thresholds. Inaccurate quorum values could lead to misinformed governance decisions, as the required participation threshold might not be accurately determined.

Proof of Concept

The vulnerability lies in the quorum function of the contract, where the quorum is calculated using the following code snippet:

function quorum(uint256 blockNumber) public view virtual override returns (uint256) {
    return (getPastCirculatingSupply(blockNumber) * quorumNumerator(blockNumber))
        / quorumDenominator();
}

The multiplication of getPastCirculatingSupply(blockNumber) and quorumNumerator(blockNumber) could potentially result in a non-integer value. However, the subsequent division by quorumDenominator() is performed as integer division, leading to potential truncation of the decimal part and precision loss.

Tools Used

Manual

Recommended Mitigation Steps

To mitigate the potential precision loss in the quorum calculation, consider using fixed-point arithmetic libraries or mechanisms that handle decimal values with greater precision. Alternatively, you could explore increasing the denominator value beyond 10,000 to reduce the impact of truncation on the calculated quorum.

Assessed type

Math

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

Lack of Validation for Maximum Candidates from the Same Organization in Security Council Replacement

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/SecurityCouncilMgmtUpgradeLib.sol#L8-L27

Vulnerability details

The contract code responsible for performing security council management upgrades in the Arbitrum DAO lacks proper validation to enforce the rule that limits the number of candidates from the same organization in the Security Council. This oversight could potentially lead to the violation of this rule, allowing more than three candidates from the same organization to be elected into the Security Council.

Impact

The lack of validation for the maximum number of candidates from the same organization in the Security Council replacement process could lead to an imbalance in the council's composition. If this vulnerability is exploited, more than three candidates from a single organization could potentially gain access to the Security Council, undermining the democratic and diverse representation that the Arbitrum DAO aims to achieve.

Proof of Concept

In the provided contract code, the replaceEmergencySecurityCouncil function within the SecurityCouncilMgmtUpgradeLib library facilitates the replacement of the emergency security council. However, this function does not include logic to validate the organization affiliations of the candidates being added to the Security Council. As a result, it does not consider the rule from the Arbitrum's documentation that restricts the number of candidates from the same organization to a maximum of three.

function replaceEmergencySecurityCouncil(
    IGnosisSafe _prevSecurityCouncil,
    IGnosisSafe _newSecurityCouncil,
    uint256 _threshold,
    IUpgradeExecutor _upgradeExecutor
) internal {
    // Existing logic for role and threshold replacement

    // Missing validation for maximum candidates from the same organization
}

Tools Used

Manual

Recommended Mitigation Steps

To mitigate this issue, the contract code should be enhanced with a validation mechanism that checks the organization affiliations of candidates during the replacement of the emergency security council. This validation should ensure that no more than three candidates from the same organization are elected to the Security Council. By enforcing this rule, the contract code will align with the governance guidelines set forth in the Arbitrum's documentation and prevent the potential imbalance caused by an overrepresentation of candidates from a single organization.

Assessed type

Invalid Validation

User that has arb tokens will not be able to use it for voting, if it was received in same l1 block as proposal was created

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/L2ArbitrumToken.sol#L26

Vulnerability details

Impact

User that has arb tokens will not be able to use it for voting, if it was received in same l1 block as proposal was created

Proof of Concept

Arbitrum election system uses L2ArbitrumToken for voting.
L2ArbitrumToken extends ERC20VoteUpgradeable which has getPastVotes functioin which is used for fetching votes on voting:

    function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) {
        require(blockNumber < block.number, "ERC20Votes: block not yet mined");
        return _checkpointsLookup(_checkpoints[account], blockNumber);
    }

    function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) {
        require(blockNumber < block.number, "ERC20Votes: block not yet mined");
        return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber);
    }

As you can see, these functions allow fetching votes only for block that is less than block.number.
This is done in order to disallow voting, sending tokens to another account and voting again in same block.

Now, let's look what is block.number in arbitrum.

block.number corresponds to the l1 block, which contains several arbitrum blocks.
In example in the docs we can see that arbitrum blocks 370000, 370005, 370006, 370008 are all in same block.number == 1000.

Because of that next situation is possible.
User who received arb tokens at arbitrum block 370000 which is block.number 1000 can't use them to vote on proposal that started at block 370005(which is later, but still also block.number 1000).
But intuitively he should be able to do so and it looks like ux problem.

Tools Used

VsCode

Recommended Mitigation Steps

Arbitrum blocks should be used inside governor contracts and inherited contracts(but i understand that it's more convenient to use original one).

Assessed type

Error

Unwelcome election cannot be canceled

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol#L161

Vulnerability details

Impact

If the protocol is not in a good situation for running an election, reaching to election time gives the power to anyone to create an election which can impact on timing of the protocol.

Proof of Concept

Suppose that it is time for a nominee election, but the protocol is not in a good situation for running an election, like:

  • The election front-end is not working properly.
  • The protocol has noticed that some of the members are malicious.
  • A recent incident has happened that it has much more priority to deal with.
  • ArbitrumOne is under heavy update that takes time.

So, if anyone calls the function createElection, the election proposal starts, and it increases the election count by one.
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol#L161C14-L161C28
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol#L180

So, it can not be cancelled, because the state of proposal will go to Active mode immediately as the voting delay is equal to zero.
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol#L108
https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/3d4c0d5741b131c231e558d7a6213392ab3672a5/contracts/governance/GovernorUpgradeable.sol#L296
https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/3d4c0d5741b131c231e558d7a6213392ab3672a5/contracts/governance/GovernorUpgradeable.sol#L361

This is not very welcome for the protocol, because as mentioned above, the protocol is not in a good situation for election. Now that the election is started:

Or suppose that the protocol is in a good situation for an election, but during voting period, the chain suddenly goes down, so those who voted already are the only ones, and no new vote can be casted. After the chain is awake, the election should be invalidated, because for example, among to-be-14-day-voting-period, only 5 days was possible to vote.

Or suppose that the March election was not possible to be executed because the protocol was down during that time. Then around September the protocol is up, so creating election is possible. So, two elections March and September can be executed at the same time which can be troublesome.

Tools Used

Recommended Mitigation Steps

  • The election cancellation should be possible by 9/12 security council members.
  • Cancelation of an election should reduce the election count as well to not face with issues explained above.
  • There should be an expiration date for executing an election. If it passes the expiration date, creating it should only increments the election count.

Assessed type

Context

DAO can change fullWeightDuration during an ongoing SC election

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L77-L85
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L241-L255
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L164-L167

Vulnerability details

Summary

In SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol, fullWeightDuration is used in votesToWeight (called in _countVote) to determine when voting weight starts linearly decreasing, and to calculate the weight of the a user's vote during this period. SecurityCouncilMemberElectionGovernor.sol inherits from SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol which contains setFullWeightDuration allowing governance to change fullWeightDuration on execution of the effects of an AIP (provided it is <= votingPeriod), even during an election (this can occur since elections can be started at any time by any address). This violates the restriction in the constitution that

The DAO may approve and implement a Constitutional AIP to change the rules governing future Security >Council elections, but the AIP process may not be used to intervene in an ongoing election.

An increase in fullWeightDuration could effectively increase the voting weight allotted to voters who still haven't voted, or have remaining votes, while a decrease could result in voters losing voting weight despite expecting to still have remaining time to cast full weight votes. This could significantly sway the results of an SC election depending on the magnitude of the change in fullWeightDuration, and when it is executed. (Note while the SC could cancel the AIP while it is not passed, this is a direct violation of the constitution and the SC's authority could change in the future)

Impact

Violation of the constitution and possible damage to the integrity of the SC member election process.

Tools Used

Manual Review

Recommended Mitigation Steps

Considering saving the fullWeightDuration at the start of an election and use that to calculate voting weight, or validate an election is not ongoing when setFullWeightDuration is called.

Assessed type

Other

Lack of Conflict of Interest Checks

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/gov-action-contracts/AIPs/SecurityCouncilMgmt/NonGovernanceChainSCMgmtActivationAction.sol#L1-L45

Vulnerability details

The contract code does not include any mechanism to check for conflicts of interest among the candidates being added or replaced in the Security Council. This lack of conflict of interest checks could potentially allow candidates with conflicting interests to be elected into the Security Council, which might compromise the integrity of the council's decisions.

Impact

If this vulnerability is exploited, it could result in candidates with conflicts of interest holding positions in the Security Council. This could lead to biased decision-making that prioritizes personal or organizational gains over the broader interests of the DAO and its community. Such a compromise in the decision-making process could erode the trust and credibility of the Security Council and the governance mechanisms of the Arbitrum DAO.

Proof of Concept

The contract code, as provided, does not contain any code to verify whether the candidates being added or replaced in the Security Council have conflicts of interest that could hinder their ability to act in the best interests of the Arbitrum DAO. The absence of conflict of interest checks leaves room for individuals who may have vested interests or affiliations that could impact their decision-making within the council.

Tools Used

Manual

Recommended Mitigation Steps

To address this security concern, the contract code should be enhanced to include a mechanism for validating and checking conflicts of interest among candidates. This could involve assessing candidates' affiliations, relationships, and interests to ensure that they do not have any potential conflicts that could compromise their ability to make impartial decisions for the benefit of the DAO. By implementing conflict of interest checks, the contract code would align with the governance principles of transparency, fairness, and ethical decision-making.

Assessed type

Access Control

Agreements & Disclosures

Agreements

If you are a C4 Certified Contributor by commenting or interacting with this repo prior to public release of the contest report, you agree that you have read the Certified Warden docs and agree to be bound by:

To signal your agreement to these terms, add a ๐Ÿ‘ emoji to this issue.

Code4rena staff reserves the right to disqualify anyone from this role and similar future opportunities who is unable to participate within the above guidelines.

Disclosures

Sponsors may elect to add team members and contractors to assist in sponsor review and triage. All sponsor representatives added to the repo should comment on this issue to identify themselves.

To ensure contest integrity, the following potential conflicts of interest should also be disclosed with a comment in this issue:

  1. any sponsor staff or sponsor contractors who are also participating as wardens
  2. any wardens hired to assist with sponsor review (and thus presenting sponsor viewpoint on findings)
  3. any wardens who have a relationship with a judge that would typically fall in the category of potential conflict of interest (family, employer, business partner, etc)
  4. any other case where someone might reasonably infer a possible conflict of interest.

Voting System Limitation - Lack of Comprehensive Voting Options in SecurityCouncilMemberElectionGovernorCountingUpgradeable function

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L95-L140

Vulnerability details

The voting system in the provided contract lacks comprehensive voting options, as it enforces a fixed "support" value of 1 for all votes, thereby not allowing participants to express dissent, abstain from voting, or provide nuanced opinions.

Impact

The lack of comprehensive voting options can have several impacts:

  • Lack of Expression: Participants are unable to express their genuine opinions, dissent, or abstention, leading to a less accurate representation of voter sentiment.
  • Unintended Outcomes: The enforced "support" value might lead to unintended outcomes, as participants could cast votes merely for participation rather than genuine agreement.
  • Undermined Democratic Process: The voting system may not adhere to democratic principles by not allowing participants to fully engage in the decision-making process.

Proof of Concept

The contract enforces a "support" value of 1 for all votes cast by participants. This approach is evident in the _countVote function, where the support parameter is checked and enforced to be equal to 1. This limitation restricts participants from expressing their dissent, opposition, or abstention from voting. The contract assumes that all votes are in favor of the proposal without considering the diverse range of opinions that participants might have.

function _countVote(
    uint256 proposalId,
    address account,
    uint8 support,
    uint256 availableVotes,
    bytes memory params
) internal virtual override {
    if (support != 1) {
        revert InvalidSupport(support);
    }
    // ... other code ...
}

Tools Used

Manual

Recommended Mitigation Steps

To address this issue, the voting mechanism should be enhanced to include comprehensive voting options such as "support," "oppose," and "abstain." This can be achieved by modifying the contract's logic to allow for different values of the support parameter, reflecting a more accurate representation of voter preferences. Implementing a flexible voting mechanism would require updating the contract's code and integrating a broader range of voting choices, thereby ensuring a more inclusive and democratic decision-making process.

Assessed type

Governance

Constructor not executed properly

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilMemberSyncAction.sol#L21

Vulnerability details

Impact

Detailed description of the impact of this finding.
The curly braces are misplaced in the constructor. The code that is intended to be executed within the constructor, the ActionExecutionRecord(), does not work as intended because every time the contract is called the constructor can be executed more than once.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Tools Used

Manual analysis

Recommended Mitigation Steps

Correct code:
constructor(KeyValueStore _store){
ActionExecutionRecord(_store, "SecurityCouncilMemberSyncAction")
}

Assessed type

Error

Consider making contracts pausable by using PauseableUpgradeable.sol

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L28
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol#L16

Vulnerability details

Impact

Detailed description of the impact of this finding.

Pausable functionality are useful during contract upgrades. When deploying a new version of the contract, its advisable to pause the old contract to prevent any new actions from being taken while migrating data or performing other maintenance tasks. Using Access control, you can define who has the authority to pause the contract and under what circumstances.

Also having a pause feature can help mitigate potential risks in case of critical bugs or vulnerabilities in the contract discovered later on. By pausing the contract, you can prevent any further execution and potential harm while you investigate and fix the issue.

Proof of Concept

Provide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.

Tools Used

Manual Review

Recommended Mitigation Steps

Use PausableUpgradeable.sol by Open-zeppelin

Assessed type

Other

owner can double vote in the intialize() .

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/FixedDelegateErc20Wallet.sol#L21-L32

Vulnerability details

Impact

If the delegateTo address and the owner address are the same in the provided function, then the contract will still deploy and execute without any issues based on the code you've provided. However, it's important to consider the implications of having the same address for both delegateTo and owner.

In this specific context, if the delegateTo address and the owner address are the same, it would mean that the owner of the contract is also delegating their voting power to themselves.

Proof of Concept

   function initialize(address token, address delegateTo, address owner) public initializer {
    require(token != address(0), "FixedDelegateErc20Wallet: zero token address");
    require(delegateTo != address(0), "FixedDelegateErc20Wallet: zero delegateTo address");
    require(owner != address(0), "FixedDelegateErc20Wallet: zero owner address");

    __Ownable_init();

    IVotesUpgradeable voteToken = IVotesUpgradeable(token);
    voteToken.delegate(delegateTo);

    _transferOwnership(owner);
}

Tools Used

Manual review

Recommended Mitigation Steps

Add two require statements to check that the owner address must not be equal to delegateTo and owner address
must not be equal to token address.
This function is more secure than above.

    function initialize(address token, address delegateTo, address owner) public initializer {
    require(token != address(0), "FixedDelegateErc20Wallet: zero token address");
    require(delegateTo != address(0), "FixedDelegateErc20Wallet: zero delegateTo address");
    require(owner != address(0), "FixedDelegateErc20Wallet: zero owner address");
    require(owner != delegateTo, "FixedDelegateErc20Wallet: Owner cannot delegate to itself");
   require(owner != token, "FixedDelegateErc20Wallet: Owner address cannot be the same as token address");

    __Ownable_init();

    IVotesUpgradeable voteToken = IVotesUpgradeable(token);
    voteToken.delegate(delegateTo);

    _transferOwnership(owner);
}

Assessed type

Governance

SecurityCouncilMemberElectionGovernor can be initialized with owner=address(0x0)

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/SecurityCouncilMemberElectionGovernor.sol#L42-L76

Vulnerability details

Impact

Owner of SecurityCouncilMemberElectionGovernor can be setted as address(0x0). This implies that nobody can be owner of the contract anymore and developers has to redeploy contract.

Proof of Concept

Using visual ispection, we found that inside SecurityCouncilMemberElectionGovernor.initialize() is used _transferOwnership(_owner) at line 66:

42      /// @param _nomineeElectionGovernor The SecurityCouncilNomineeElectionGovernor
43      /// @param _securityCouncilManager The SecurityCouncilManager
44      /// @param _token The token used for voting
45      /// @param _owner The owner of the governor
46      /// @param _votingPeriod The duration of voting on a proposal
47      /// @param _fullWeightDuration Duration of full weight voting (blocks)
48      function initialize(
49          ISecurityCouncilNomineeElectionGovernor _nomineeElectionGovernor,
50          ISecurityCouncilManager _securityCouncilManager,
51          IVotesUpgradeable _token,
52          address _owner,
53          uint256 _votingPeriod,
54          uint256 _fullWeightDuration
55      ) public initializer {
56          if (_fullWeightDuration > _votingPeriod) {
57              revert InvalidDurations(_fullWeightDuration, _votingPeriod);
58          }
59  
60          __Governor_init("SecurityCouncilMemberElectionGovernor");
61          __GovernorVotes_init(_token);
62          __SecurityCouncilMemberElectionGovernorCounting_init({
63              initialFullWeightDuration: _fullWeightDuration
64          });
65          __GovernorSettings_init(0, _votingPeriod, 0);
66          _transferOwnership(_owner);
67  
68          if (!Address.isContract(address(_nomineeElectionGovernor))) {
69              revert NotAContract(address(_nomineeElectionGovernor));
70          }
71          nomineeElectionGovernor = _nomineeElectionGovernor;
72          if (!Address.isContract(address(_securityCouncilManager))) {
73              revert NotAContract(address(_securityCouncilManager));
74          }
75          securityCouncilManager = _securityCouncilManager;
76      }

_transferOwnership is function from @openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:

93      /**
94       * @dev Transfers ownership of the contract to a new account (`newOwner`).
95       * Internal function without access restriction.
96       */
97      function _transferOwnership(address newOwner) internal virtual {
98          address oldOwner = _owner;
99          _owner = newOwner;
100          emit OwnershipTransferred(oldOwner, newOwner);
101      }

So, this function doen't made any check on address newOwner

We also made test to initialize SecurityCouncilMemberElectionGovernor with owner=address(0x0):

Test description

contract SecurityCouncilMemberElectionGovernorTest is Test {
    struct InitParams {
        ISecurityCouncilNomineeElectionGovernor nomineeElectionGovernor;
        ISecurityCouncilManager securityCouncilManager;
        IVotesUpgradeable token;
        address owner;
        address owner_0;
        uint256 votingPeriod;
        uint256 maxNominees;
        uint256 fullWeightDuration;
    }

    SecurityCouncilMemberElectionGovernor governor;
    address proxyAdmin = address(0x11);

    InitParams initParams = InitParams({
        nomineeElectionGovernor: ISecurityCouncilNomineeElectionGovernor(payable(address(0x22))),
        securityCouncilManager: ISecurityCouncilManager(address(0x33)),
        token: IVotesUpgradeable(address(0x44)),
        owner: address(0x55),
        owner_0: address(0x0),
        votingPeriod: 2 ** 8,
        maxNominees: 6,
        fullWeightDuration: 2 ** 7
    });
[...]
    function testInitReverts2() public {
        SecurityCouncilMemberElectionGovernor governor2 = _deployGovernor();
        governor2.initialize({
            _nomineeElectionGovernor: initParams.nomineeElectionGovernor,
            _securityCouncilManager: initParams.securityCouncilManager,
            _token: initParams.token,
            _owner: initParams.owner_0,
            _votingPeriod: initParams.votingPeriod,
            _fullWeightDuration: initParams.fullWeightDuration
        });
    }
}

Test result

[PASS] testInitReverts2() (gas: 5091697)

Tools Used

Visual ispection and Foundry

Recommended Mitigation Steps

You could use transferOwnership() instead of _transferOwnership():

82      /**
83       * @dev Transfers ownership of the contract to a new account (`newOwner`).
84       * Can only be called by the current owner.
85       */
86      function transferOwnership(address newOwner) public virtual onlyOwner {
87          if (newOwner == address(0)) {
88              revert OwnableInvalidOwner(address(0));
89          }
90          _transferOwnership(newOwner);
91      }

or check address inside SecurityCouncilMemberElectionGovernor.initialize()

Assessed type

Invalid Validation

QA Report

See the markdown file with the details of this report here.

QA Report

See the markdown file with the details of this report here.

Insufficient check for cohorts initialization

Lines of code

https://github.com/arbitrumfoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L100-#L101

Vulnerability details

Impact

  • Cohorts could be set with zero length, after that admin/managers cannot add new member
  • One member could be in both cohorts
  • Duplicates in cohorts

Proof of Concept

Currently, function initialize of contract SecurityCouncilManager is implemented as:

function initialize(
        address[] memory _firstCohort,
        address[] memory _secondCohort,
        SecurityCouncilData[] memory _securityCouncils,
        SecurityCouncilManagerRoles memory _roles,
        address payable _l2CoreGovTimelock,
        UpgradeExecRouteBuilder _router
    ) external initializer {
        if (_firstCohort.length != _secondCohort.length) {
            revert CohortLengthMismatch(_firstCohort, _secondCohort);
        }
        firstCohort = _firstCohort;
        secondCohort = _secondCohort;
        cohortSize = _firstCohort.length;
        _grantRole(DEFAULT_ADMIN_ROLE, _roles.admin);
        _grantRole(COHORT_REPLACER_ROLE, _roles.cohortUpdator);
        _grantRole(MEMBER_ADDER_ROLE, _roles.memberAdder);
        for (uint256 i = 0; i < _roles.memberRemovers.length; i++) {
            _grantRole(MEMBER_REMOVER_ROLE, _roles.memberRemovers[i]);
        }
        _grantRole(MEMBER_ROTATOR_ROLE, _roles.memberRotator);
        _grantRole(MEMBER_REPLACER_ROLE, _roles.memberReplacer);

        if (!Address.isContract(_l2CoreGovTimelock)) {
            revert NotAContract({account: _l2CoreGovTimelock});
        }
        l2CoreGovTimelock = _l2CoreGovTimelock;

        _setUpgradeExecRouteBuilder(_router);
        for (uint256 i = 0; i < _securityCouncils.length; i++) {
            _addSecurityCouncil(_securityCouncils[i]);
        }
    }

As you can see the only check on firstCohort and secondCohort is assuring their length are the same. This is insufficient and can cause many issues, for example:

  • The firstCohort and secondCohort could be of any size, violating the Arbitrum Constitution https://docs.arbitrum.foundation/dao-constitution#section-3-the-security-council, in which it states The Security Council is a committee of 12 members who are signers of a multi-sig wallet. Since variable cohortSize is set as the length of firstCohort and secondCohort at initialization, a 0 size cohort cannot be replaced by calling replaceCohort or added new member by calling addMember. The protocol will basically fail to function.
  • One member could end up in both cohorts
  • Duplicates in both cohorts

Below is a POC, save it as governance/test/security-council-mgmt/SecurityCouncilManagerCohortsCheck.t.sol and run it using command:
forge test --match-path test/security-council-mgmt/SecurityCouncilManagerCohortsCheck.t.sol -vvvv

// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.16;

import "forge-std/Test.sol";
import "../../src/security-council-mgmt/SecurityCouncilManager.sol";
import "../../src/UpgradeExecRouteBuilder.sol";

import "../util/TestUtil.sol";
import "../util/MockArbSys.sol";
import "../../src/security-council-mgmt/Common.sol";

contract MockArbitrumTimelock {
	event CallScheduled(
        bytes32 indexed id,
        uint256 indexed index,
        address target,
        uint256 value,
        bytes data,
        bytes32 predecessor,
        uint256 delay
    );

    function getMinDelay() external view returns (uint256){
    	return uint256(123);
    }

    function schedule(
    	address target,
    	uint256 value,
    	bytes calldata data,
    	bytes32 predecessor,
    	bytes32 salt,
    	uint256 delay
    ) public virtual {
    	emit CallScheduled(salt, 0, target, value, data, predecessor, delay);
    }
}

contract SecurityCouncilManagerCohortsCheckTest is Test {
	address[] firstCohort = new address[](6);
	address[6] _firstCohort
	=  [address(1111), address(1112), address(1113), address(1114), address(1115), address(1116)];

	address[] secondCohort = new address[](6);
    address[6] _secondCohort =
        [address(2221), address(2222), address(2223), address(2224), address(2225), address(2226)];

    address[] newCohort = new address[](6);
    address[6] _newCohort =
        [address(3331), address(3332), address(3333), address(3334), address(3335), address(3336)];

    address[] newCohortWithADup = new address[](6);
    address dup = address(3355);
    address[6] _newCohortWithADup
    = [address(3331), address(3332), address(3333), address(3334), dup, dup];

    SecurityCouncilManager scm;
    UpgradeExecRouteBuilder uerb;
    address[] memberRemovers = new address[](2);
    address memberRemover1 = address(4444);
    address memberRemover2 = address(4445);

    SecurityCouncilManagerRoles roles = SecurityCouncilManagerRoles({
    	admin: address(4441),
    	cohortUpdator: address(4442),
    	memberAdder: address(4443),
    	memberRemovers: memberRemovers,
    	memberRotator: address(4446),
    	memberReplacer: address(4447)
    });

    address rando = address(6661);
    address memberToAdd = address(7771);

    address l1ArbitrumTimelock = address(8881);

    address payable l2CoreGovTimelock;

    uint256 l1TimelockMinDelay = uint256(1);

    ChainAndUpExecLocation[] chainAndUpExecLocation;
    SecurityCouncilData[] securityCouncils;

    SecurityCouncilData firstSc = SecurityCouncilData({
    	securityCouncil: address(9991),
    	updateAction: address(9992),
    	chainId: 2
    });


    SecurityCouncilData scToAdd = SecurityCouncilData({
        securityCouncil: address(9993),
        updateAction: address(9994),
        chainId: 3
    });

    ChainAndUpExecLocation firstChainAndUpExecLocation = ChainAndUpExecLocation({
        chainId: 2,
        location: UpExecLocation({inbox: address(9993), upgradeExecutor: address(9994)})
    });

    ChainAndUpExecLocation secondChainAndUpExecLocation = ChainAndUpExecLocation({
        chainId: 3,
        location: UpExecLocation({inbox: address(9995), upgradeExecutor: address(9996)})
    });


    address[] bothCohorts;

    function setUp() public {
    	chainAndUpExecLocation.push(firstChainAndUpExecLocation);
    	chainAndUpExecLocation.push(secondChainAndUpExecLocation);

    	uerb = new UpgradeExecRouteBuilder({
    		_upgradeExecutors: chainAndUpExecLocation,
    		_l1ArbitrumTimelock: l1ArbitrumTimelock,
    		_l1TimelockMinDelay: l1TimelockMinDelay
    	});

    	for (uint256 i=0; i< 6; i++){
    		secondCohort[i] = _secondCohort[i];
    		firstCohort[i] = _firstCohort[i];
    		bothCohorts.push(_firstCohort[i]);
    		bothCohorts.push(_secondCohort[i]);
    		newCohort[i] = _newCohort[i];
    		newCohortWithADup[i] = _newCohortWithADup[i];
    	}

    	address prox = TestUtil.deployProxy(address(new SecurityCouncilManager()));
    	scm = SecurityCouncilManager(payable(prox));

    	l2CoreGovTimelock = payable(address(new MockArbitrumTimelock()));
    	securityCouncils.push(firstSc);

    	
    }

    function testCohortEmpty() public {

        // firstCohort and secondCohort length = 0
        address[] memory newFirstCohort = new address[](0);
        address[] memory newSecondCohort = new address[](0);

        scm.initialize(
            newFirstCohort,
            newSecondCohort,
            securityCouncils,
            roles,
            l2CoreGovTimelock,
            uerb
        );

        assertEq(scm.cohortSize(), 0);

        // Cannot add member since cohort size = 0
        vm.prank(roles.memberAdder);
        vm.expectRevert(
            abi.encodeWithSelector(ISecurityCouncilManager.CohortFull.selector, Cohort.FIRST)
        );
        scm.addMember(address(0x1111), Cohort.FIRST);
    }

    function testMemberInBothCohorts() public {
        
        address[] memory newFirstCohort = new address[](6);
        newFirstCohort[0] = address(1111);
        newFirstCohort[1] = address(1112);
        newFirstCohort[2] = address(1113);
        newFirstCohort[3] = address(1114);
        newFirstCohort[4] = address(1115);
        newFirstCohort[5] = address(1116);

        address[] memory newSecondCohort = newFirstCohort;

        // Successfully initialize with first firstCohort = secondCohort
        scm.initialize(
            newFirstCohort,
            newSecondCohort,
            securityCouncils,
            roles,
            l2CoreGovTimelock,
            uerb
        );
        
        assertTrue(
            TestUtil.areAddressArraysEqual(newFirstCohort, scm.getFirstCohort()),
            "first cohort is set"
        );

        assertTrue(
            TestUtil.areAddressArraysEqual(newFirstCohort, scm.getSecondCohort()),
            "second cohort is the same as first cohort"
        );

        // Cannot remove duplicated member
        address memberToRemove = firstCohort[0];
        vm.prank(roles.memberRemovers[0]);
        scm.removeMember(memberToRemove);

        // Member is still in cohort
        assertTrue(scm.cohortIncludes(Cohort.SECOND, memberToRemove));

    }


 }

Tools Used

Manual review

Recommended Mitigation Steps

I recommend making additional checks on firstCohort and secondCohort to make sure that:

  • Their lengths are 6 (according to the constitution)
  • They don't contain duplicate member (both in the same cohort and across cohort)

Assessed type

Invalid Validation

`SecurityCouncilNomineeElectionGovernorTiming.electionToTimestamp(...)` can create unsupported/invalid dates

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilNomineeElectionGovernorTiming.sol#L73-L94

Vulnerability details

Impact

The SecurityCouncilNomineeElectionGovernorTiming.electionToTimestamp(uint256 electionIndex) method basically adds 6 months * electionIndex to the firstNominationStartDate and returns the resulting timetamp.
The issue is that firstNominationStartDate.day is just passed trough without changes, see L89.
As a consquence, depending on the firstNominationStartDate, unsupported/invalid dates. which exceed the "days in month" (examples: 2030-09-31, 2030-02-29). are generated before the timestamp creation which leads to undefined behaviour in DateTimeLib.dateTimeToTimestamp(...), see also DateTimeLib.isSupportedDateTime(...).

The SecurityCouncilNomineeElectionGovernor.createElection() method is directly affected by the above issue since the creation of a new election might fail or be available too early due to relying on a timestamp whose creation was subject to undefined behaviour.

Keep in mind, that although there might be no problem, i.e. valid timestamps, with most invalid dates using the current library version, it is still undefined behaviour and one has to pay extra attention in case of using another version of the DateTimeLib.

Proof of Concept

The following PoC implicitly modifies the existing testCreateElection case to demonstrate the unsupported/invalid date issue.
Just apply the diff below and run the tests with forge test -vv --match-contract SecurityCouncilNomineeElectionGovernorTest.

diff --git a/src/security-council-mgmt/governors/modules/SecurityCouncilNomineeElectionGovernorTiming.sol b/src/security-council-mgmt/governors/modules/SecurityCouncilNomineeElectionGovernorTiming.sol
index c8fd056..8b5ff35 100644
--- a/src/security-council-mgmt/governors/modules/SecurityCouncilNomineeElectionGovernorTiming.sol
+++ b/src/security-council-mgmt/governors/modules/SecurityCouncilNomineeElectionGovernorTiming.sol
@@ -83,6 +83,16 @@ abstract contract SecurityCouncilNomineeElectionGovernorTiming is
         // add one to make month 1 indexed
         month += 1;
 
+        // PoC: add check as suggested by DateTimeLib
+        require(DateTimeLib.isSupportedDateTime({
+            year: year,
+            month: month,
+            day: firstNominationStartDate.day,
+            hour: firstNominationStartDate.hour,
+            minute: 0,
+            second: 0
+        }), "PoC: Unsupported election DateTime");
+
         return DateTimeLib.dateTimeToTimestamp({
             year: year,
             month: month,
diff --git a/test/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.t.sol b/test/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.t.sol
index 9f60ba5..5dc99e0 100644
--- a/test/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.t.sol
+++ b/test/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.t.sol
@@ -17,7 +17,8 @@ contract SecurityCouncilNomineeElectionGovernorTest is Test {
 
     SecurityCouncilNomineeElectionGovernor.InitParams initParams =
     SecurityCouncilNomineeElectionGovernor.InitParams({
-        firstNominationStartDate: Date({year: 2030, month: 1, day: 1, hour: 0}),
+        firstNominationStartDate: Date({year: 2030, month: 3, day: 31, hour: 0}),
+        // other: firstNominationStartDate: Date({year: 2029, month: 8, day: 29, hour: 0}),
         nomineeVettingDuration: 1 days,
         nomineeVetter: address(0x11),
         securityCouncilManager: ISecurityCouncilManager(address(0x22)),

Tools Used

VS Code, Foundry

Recommended Mitigation Steps

Use the DateTimeLib.addMonths(...) method to safely add months to a given DateTime or timestamp.

Assessed type

Math

Potential Lack of Conflict of Interest Verification in Arbitrum DAO Election Process

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L1-L300

Vulnerability details

The Arbitrum DAO election process, as depicted in the provided code snippet, lacks explicit provisions for verifying and addressing conflicts of interest among elected candidates. This omission could lead to potential biases and adverse impacts on the governance process.

Impact

The absence of conflict of interest verification in the election process can have serious consequences. Elected candidates with undisclosed conflicts of interest could influence decisions in a manner that benefits themselves or entities they are associated with, rather than serving the best interests of the Arbitrum DAO and its stakeholders. This lack of transparency and accountability can erode trust in the governance process and undermine the DAO's ability to make impartial decisions.

Proof of Concept

The code snippet represents the counting module for the SecurityCouncilMemberElectionGovernor, which outlines the election and voting process for the Arbitrum DAO. While the code focuses on aspects such as vote counting and nominee selection, it does not include any specific logic or checks related to evaluating conflicts of interest among elected candidates.

Tools Used

Manual

Recommended Mitigation Steps

To address this issue, it is recommended to incorporate a comprehensive conflict of interest verification process into the election and governance framework. This process should include the following steps:

  • Disclosure: Require all candidates to disclose any potential conflicts of interest they may have, including affiliations with other projects, organizations, or entities that could influence their decision-making within the DAO.

  • Verification: Establish a mechanism to verify the accuracy of candidate disclosures. This could involve conducting background checks, verifying affiliations, and assessing potential conflicts based on established criteria.

  • Evaluation: Develop clear criteria for evaluating the severity and relevance of disclosed conflicts. Define thresholds beyond which a conflict may disqualify a candidate from standing for election.

  • Transparency: Ensure that candidate disclosures and conflict assessments are publicly accessible to DAO members. Transparency will enable stakeholders to make informed decisions during the election process.

  • Enforcement: If a candidate is elected and subsequently found to have undisclosed conflicts of interest, establish a process for addressing the situation. This could involve reevaluating the candidate's eligibility, conducting investigations, and potentially taking corrective actions, such as removal from the elected position.

Assessed type

Access Control

Safe owners can collude to make elections not possible

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilMemberSyncAction.sol#L31-L74

Vulnerability details

Impact

Safe owners can collude to make elections not possible.

Proof of Concept

In order to be able to manage owners of gnosis safe wallets, SecurityCouncilMemberSyncAction contract will be used. UpgradeExecutor will be registered as module of wallets and it will delegate call into SecurityCouncilMemberSyncAction.perform function to change owners after changes inside SecurityCouncilManager.

The problem is that current members of wallet can collude to make it not possible to do changes anymore.
Non emergency wallet has 7/12 threshold, which means that in order to execute tx, 7 out of 12 members is needed.
When new elections come, then 6 members are reelected. That makes it's possible that previous 6 members will collude and they need only 1 more member, that they can bribe.

Once they have 7 members, then they control wallet and can change remove UpgradeExecutor as module from safe wallet. Once it's done, then it will be not possible to change owners anymore.

Tools Used

VsCode

Recommended Mitigation Steps

I don't have, such possibility will always exist.

Assessed type

Error

Outdated Versions of "@openzeppelin/contracts" and "@openzeppelin/contracts-upgradeable

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/package.json#L69

Vulnerability details

Impact

usage of outdated versions of the "@openzeppelin/contracts" and "@openzeppelin/contracts-upgradeable" packages.

The vulnerable versions being used are:

"@openzeppelin/contracts": "4.7.3"
"@openzeppelin/contracts-upgradeable": "4.7.3" These version are vulnerable for very known bugs and CVE

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/package.json#L69C4-L70C52

Tools Used

mannual

Recommended Mitigation Steps

@openzeppelin/contracts" and "@openzeppelin/contracts-upgradeable" to their latest versions

Assessed type

Library

An excluded nominee cannot be included will be an issue in cases when nominees are fewer than the target

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol#L296

Vulnerability details

Impact

Since _elections mapping in SecurityCouncilNomineeElectionGovernorCountingUpgradeable and SecurityCouncilNomineeElectionGovernor do not track each other, it can result in issues when the number of nominees is fewer than the target.

Proof of Concept

Suppose during nominee election, Bob receives enough vote to be added to the list of nominees.
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilNomineeElectionGovernorCountingUpgradeable.sol#L104
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilNomineeElectionGovernorCountingUpgradeable.sol#L121

So, we have:

SecurityCouncilNomineeElectionGovernorCountingUpgradeable::_elections[proposalId].nominees.push(Bob);
SecurityCouncilNomineeElectionGovernorCountingUpgradeable::_elections[proposalId].isNominee[Bob] = true;

After the deadline (when the voting period is ended), the nomineeVetter decides to exclude Bob for any reason.
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol#L266

So, we have:

 SecurityCouncilNomineeElectionGovernor::_elections.isExcluded[Bob] = true;
 SecurityCouncilNomineeElectionGovernor::_elections.excludedNomineeCount++;

After the vetting period, the nomineeVetter notices that there are fewer nominees than the target. So, nomineeVetter decides to include Bob as nominee again. But since the storage variables in SecurityCouncilNomineeElectionGovernorCountingUpgradeable and SecurityCouncilNomineeElectionGovernor are not tracking each other, this action will revert.

Because, SecurityCouncilNomineeElectionGovernorCountingUpgradeable states that Bob is a nominee, but SecurityCouncilNomineeElectionGovernor states that Bob is an excluded nominee.
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/SecurityCouncilNomineeElectionGovernor.sol#L296

This condition limits the nomineeVetter in cases when the number of nominees is fewer than the target, and the previous cohort members inclusion is not possible or still does not meet the target.

Tools Used

Recommended Mitigation Steps

These two storage variables should track each other.
Or, when a nominee is excluded, it should be removed from the _elections in SecurityCouncilNomineeElectionGovernorCountingUpgradeable as well.
It requires some modifications in the code.

Assessed type

Context

initialize function doesn't check if members of the firstCohort are not in the secondCohort

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L89C3-L121C6 #L89-#L121

Vulnerability details

Impact

Members of the firstCohort can also be included in the members of the secondCohort through the initialize function.

Proof of Concept

"Security Council member in one cohort may not be a candidate for a seat in the other cohort" - see https://docs.arbitrum.foundation/dao-constitution

initialize function allows members of the first and second cohorts to be entered. However, there is no check to ensure that members of the first cohort are not part of the second cohort as required by the Arbitrum constitution.

Tools Used

Manual review

Recommended Mitigation Steps

Add check to ensure members of the first cohort cannot be added to the second cohort.

Assessed type

Governance

Inadequate Nonce Handling for Security Council Updates

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilMemberSyncAction.sol#L31-L74

Vulnerability details

The contract's nonce handling mechanism for security council updates is not fully aligned with the Constitution of the Arbitrum DAO, which emphasizes the importance of a nonce greater than the stored nonce for proper execution of updates. This mismatch might lead to discrepancies between the contract's execution and the DAO's expected behavior.

Impact

An attacker could potentially exploit this inconsistency in nonce handling to execute updates out of sequence. This could result in unintended changes to the security council members, potentially compromising the security and integrity of the Arbitrum DAO's governance process.

Proof of Concept

The contract's perform function compares the provided _nonce with the stored nonce to determine if an update should take place. However, according to the DAO's Constitution, the nonce should be strictly greater than the stored nonce for proper sequencing of updates. The contract's logic uses the condition _nonce <= updateNonce to check if the provided nonce is less than or equal to the stored nonce. This means that even if the provided nonce is equal to the stored nonce, an update will still be executed, which might lead to unexpected changes in the security council members.
Code Snippet:

uint256 updateNonce = getUpdateNonce(_securityCouncil);
if (_nonce <= updateNonce) {
    emit UpdateNonceTooLow(_securityCouncil, updateNonce, _nonce);
    return false;
}

Tools Used

Manual

Recommended Mitigation Steps

To address this issue, the contract should modify the condition to ensure that the provided _nonce is strictly greater than the stored updateNonce. By changing the condition to _nonce < updateNonce, the contract will only allow updates with a nonce that is properly incremented. This modification will ensure that updates are executed in the correct order as specified in the Constitution. Additionally, the contract should consider implementing measures to handle nonce reset and protection against replay attacks to further enhance security.

Assessed type

Invalid Validation

Missing checks when creating a SecurityCouncilManager on council member cohorts list can break protocol invariants

Lines of code

(https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L100-L101
(https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/factories/L2SecurityCouncilMgmtFactory.sol#L105-L121

Vulnerability details

Impact

When deploying a SecurityCouncilManager, two addresses lists are provided that contain the members of the Security Council. It is a council strict requirement that no member can be in both cohorts at the same time

    // A member cannot be in both cohorts at the same time
    address[] internal firstCohort;
    address[] internal secondCohort;

It is also implied that the members of the lists must be unique between the two.

The issue is that when the SecurityCouncilManager contract is deployed the provided cohort lists are not checked for these exact scenarios. Also, the provided lists, that are passed to the L2SecurityCouncilMgmtFactory are not checked even at that level. This lack of check allows the existence of a security council member in both cohorts or wors, only 1 different address being in both lists, breaking protocol invariant.

Vulnerability Details

In the proxy initialization function of SecurityCouncilManager, nu check is done on the cohorts list, presuming the check are done by the calling factory.

        firstCohort = _firstCohort;
        secondCohort = _secondCohort;

however L2SecurityCouncilMgmtFactory::deploy does not implement proper checks, it only checks that there are as many gnosis safe owners as there are entries in the cohorts combined and that the cohort addresses are all actually safe owners

        IGnosisSafe govChainEmergencySCSafe = IGnosisSafe(dp.govChainEmergencySecurityCouncil);
        address[] memory owners = govChainEmergencySCSafe.getOwners();
        if (owners.length != (dp.firstCohort.length + dp.secondCohort.length)) {
            revert InvalidCohortsSize(owners.length, dp.firstCohort.length, dp.secondCohort.length);
        }


        for (uint256 i = 0; i < dp.firstCohort.length; i++) {
            if (!govChainEmergencySCSafe.isOwner(dp.firstCohort[i])) {
                revert AddressNotInCouncil(owners, dp.firstCohort[i]);
            }
        }


        for (uint256 i = 0; i < dp.secondCohort.length; i++) {
            if (!govChainEmergencySCSafe.isOwner(dp.secondCohort[i])) {
                revert AddressNotInCouncil(owners, dp.secondCohort[i]);
            }
        }

The above check does not, however remove the possibility of a member being in two cohorts or check for uniqueness.

An important observation is that this actually impacts protocol, as such, adding or replacing a cohort member thoroughly verifies these in SecurityCouncilManager::_addMemberToCohortArray

    function _addMemberToCohortArray(address _newMember, Cohort _cohort) internal {
        // ... code ...

        if (firstCohortIncludes(_newMember)) {
            revert MemberInCohort({member: _newMember, cohort: Cohort.FIRST});
        }
        if (secondCohortIncludes(_newMember)) {
            revert MemberInCohort({member: _newMember, cohort: Cohort.SECOND});
        }

        // ... code ...
    }

This check are missing only on initialization.

Theoretical POC:

  • case 1, adding, by mistake the same address to both cohorts
    • firstCohort addresses = [A, B, C, D]
    • secondCohort addresses = [D, F, G, H]
    • gnosis owners: [A, B, C, D, E, F, G, H]
    • checks
      • the condition that owners.length (8) == firstCohort.length (4) + secondCohort.length (4) is passed
      • condition that each cohort member is an owner of the gnosis is also passed (address are all owners, just one was mistakenly doubled in the second cohort)
  • case 2: having the same address for both cohorts
    • firstCohort addresses = [A, A, A, A]
    • secondCohort addresses = [A, A, A, A]
    • gnosis owners: [A, B, C, D, E, F, G, H]
    • checks
      • length check, as above, passes
      • owner check, as above, passes

Impact

A security member can be in both councils at the same time or all members can be the exact same address.

Tools Used

Manual review

Recommend Mitigation

To fix both scenarios:

  • check that no member of one cohort is in the other
  • check that each cohort address lists are sorted, this way you assure uniqueness

These checks should be done in SecurityCouncilManager::initialize to guarantee valid council member lists.

Assessed type

Invalid Validation

Changing Security Council does not schedule an update

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L279

Vulnerability details

Impact

Adding/removing security council does not schedule an update to all supported chains. So, during important phase of the protocol (like during finalizing the member election), the removing/adding of security council can be risky as these action are async.

Proof of Concept

Suppose a member election is ongoing, and the DEFAULT_ADMIN_ROLE decides to change the address of a security council. So, first he removes it:
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L279

Then, since the election process is async, after the member election is finished, SecurityCouncilMemberElectionGovernor will call replaceCohort in the SecurityCouncilManager:
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/SecurityCouncilMemberElectionGovernor.sol#L129
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L124

In which the schedule will be updated:
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L139

During updating the schedule, the list of current security councils will be iterated to be updated with new member on different chains:
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L426
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L392

Since, a security council of one of the chain is removed temporarily by the DEFAULT_ADMIN_ROLE, this security council will not be updated with new members.

After the update is sent, the DEFAULT_ADMIN_ROLE adds the new security council, but it is too late, because the update of new members are already scheduled.
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L271

So, it means that now we have a security council on a chain with old members, while other security councils on other chains have updated members.

The problem is that:

Tools Used

Recommended Mitigation Steps

  • Better to schedule an update for the newly-added security council when it is added.
  • Better to not allow removing security council when a member election is going to be finalized.

Assessed type

Context

Issue with adding new chain

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/UpgradeExecRouteBuilder.sol#L94

Vulnerability details

Impact

Adding a new chain to the protocol can not be done easily, it requires deployment of new UpgradeExecRouteBuilder that seems it is not the protocol's plan.

Proof of Concept

If Arbitrum is going to add new chain, it should add the data of that chain's SecurityCouncil to the array of securityCouncils:
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L271

During adding this data, it will check with the UpgradeExecRouteBuilder whether the upgradeExecutor in that chain id is defined in the upExecLocations or not.
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L247C21-L247C41
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/UpgradeExecRouteBuilder.sol#L62

Please note that the upExecLocations can only be set during constructor of the UpgradeExecRouteBuilder:
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/UpgradeExecRouteBuilder.sol#L84C13-L84C28

So, the number of supported chain ids can not be increased. Because, the function upExecLocationExists will return false for a chain id that is not defined in upExecLocations during constructor.
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/UpgradeExecRouteBuilder.sol#L94

As a result, adding security council for a new chain id in the SecurityCouncilManager will revert:
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L248

Please note that to add new chain, the protocol should deploy another UpgradeExecRouteBuilder in which the new chain id is defined in its constructor, and then the address of this newely-deployed UpgradeExecRouteBuilder should be then set in SecurityCouncilManager:
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L308

This seems something that is not very welcome with the protocol as it should deploy new UpgradeExecRouteBuilder every time a new chain is going to be supported by the protocol.

Tools Used

Recommended Mitigation Steps

There should be a function that supports adding new chain id to the UpgradeExecRouteBuilder.

Assessed type

Context

Lack of validation in all fullWeightDuration settings.

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L72
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L82

Vulnerability details

Impact

In the constructor the variable is simply defined in storage fullWeightDuration, but then it is also set in the function setFullWeightDuration() validating
that the new value be < votingPeriod().
Since the implementation of the function does not exist, it is within the project, I cannot be sure that it is important or not to validate it within the constructor.

Recommended Mitigation Steps

Check that it is not necessary to validate it in the constructor that fullWeightDuration < votingPeriod().

Assessed type

Access Control

abi.encodePacked` Allows Hash Collision

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L370

Vulnerability details

Impact

abi.encodePacked() should not be used with dynamic types when passing the result to a hash function such as keccak256(). it Allows Hash Collision

Proof of Concept

Use abi.encode() instead which will pad items to 32 bytes, which will prevent hash collisions (e.g. abi.encodePacked(0x123,0x456) => 0x123456 => abi.encodePacked(0x1,0x23456), but abi.encode(0x123,0x456) => 0x0...1230...456). โ€œUnless there is a compelling reason, abi.encode should be preferredโ€. If there is only one argument to abi.encodePacked() it can often be cast to bytes() or bytes32() instead.

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L370C3-L376C6

function generateSalt(address[] memory _members, uint256 nonce)
    external
    pure
    returns (bytes32)
{
    return keccak256(abi.encodePacked(_members, nonce));
}

Tools Used

manual

Recommended Mitigation Steps

Use abi.encode() instead

Assessed type

en/de-code

L2SecurityCouncilMgmtFactory.deploy doesn't check that cohorts don't have duplicates

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/factories/L2SecurityCouncilMgmtFactory.sol#L105-L121

Vulnerability details

Impact

L2SecurityCouncilMgmtFactory.deploy doesn't check that cohorts don't have duplicates.

Proof of Concept

L2SecurityCouncilMgmtFactory.deploy is used to deploy contracts. One of them is SecurityCouncilManager, which is initialized with 2 cohorts.
This is the check that L2SecurityCouncilMgmtFactory does.
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/factories/L2SecurityCouncilMgmtFactory.sol#L105-L121

        IGnosisSafe govChainEmergencySCSafe = IGnosisSafe(dp.govChainEmergencySecurityCouncil);
        address[] memory owners = govChainEmergencySCSafe.getOwners();
        if (owners.length != (dp.firstCohort.length + dp.secondCohort.length)) {
            revert InvalidCohortsSize(owners.length, dp.firstCohort.length, dp.secondCohort.length);
        }


        for (uint256 i = 0; i < dp.firstCohort.length; i++) {
            if (!govChainEmergencySCSafe.isOwner(dp.firstCohort[i])) {
                revert AddressNotInCouncil(owners, dp.firstCohort[i]);
            }
        }


        for (uint256 i = 0; i < dp.secondCohort.length; i++) {
            if (!govChainEmergencySCSafe.isOwner(dp.secondCohort[i])) {
                revert AddressNotInCouncil(owners, dp.secondCohort[i]);
            }
        }

It checks that gnosis safe has same amount of owners as amount 2 cohorts length. And then it goes through all member is cohort and check that it's owner of gnosis safe. Then cohorts are set without additional checks.

The problem here is that, both cohorts were not checked on duplicates as it's done when new cohort is added. So it's possible that SecurityCouncilManager will be initialized with wrong addresses in cohorts, which can break contract's logic.

Tools Used

VsCode

Recommended Mitigation Steps

Check that cohorts don't have duplicates.

Assessed type

Error

Incomplete Vote Counting for Security Council Member Removal

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/SecurityCouncilMemberRemovalGovernor.sol#L163-L174

Vulnerability details

The contract implements an incomplete vote counting mechanism for the removal of security council members, where only "support" votes are considered. This approach neglects "against" votes and potential abstentions, leading to an inaccurate representation of the voters' preferences and potentially inconsistent outcomes.

Impact

The incomplete vote counting approach introduces a significant impact on the voting process for security council member removal. It may lead to inaccurate outcomes, lack of representation of dissenting voters, potential manipulation of results, and a lack of nuance in capturing voters' intentions.

Proof of Concept

In the contract, the _countVote function, responsible for counting votes during the removal proposal, only considers support votes and treats abstain votes as against votes. This approach can lead to inconsistencies and inaccuracies in the vote count. While the specification of the DAO's constitution clearly states that abstaining is equivalent to voting against, this code implementation does not accurately reflect that intention.

function _countVote(
    uint256 proposalId,
    address account,
    uint8 support,
    uint256 weight,
    bytes memory params
) internal virtual override(GovernorCountingSimpleUpgradeable, GovernorUpgradeable) {
    if (VoteType(support) == VoteType.Abstain) {
        revert AbstainDisallowed();
    }
    GovernorCountingSimpleUpgradeable._countVote(proposalId, account, support, weight, params);
}

Tools Used

Manual

Recommended Mitigation Steps

To address this issue, the contract should be updated to consider all types of votes, including "support," "against," and "abstain." This will ensure a more comprehensive and accurate representation of the voters' preferences. The _countVote function should be modified to allow all vote types and accurately tally the total votes for and against the removal proposal. This adjustment will lead to a fairer and more reliable voting process that aligns with the principles of the Arbitrum DAO's constitution and governance standards.

Assessed type

Other

Insufficient Safeguards Against Nonce Resets and Replay Attacks

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilMemberSyncAction.sol#L31-L74

Vulnerability details

The contract lacks mechanisms to protect against nonce resets and replay attacks during security council updates. This oversight can lead to unauthorized updates being executed, potentially compromising the integrity of the security council's membership and overall governance process.

Impact

Without nonce reset protection and safeguards against replay attacks, malicious actors could potentially reuse or reset nonces to execute previously valid updates. This could result in unauthorized changes to the security council membership or replaying updates that should have been one-time actions, leading to confusion and potential disruption of the security council's functioning.

Proof of Concept

The contract does not incorporate safeguards to prevent nonce resets or address replay attacks. While the Constitution of the Arbitrum DAO doesn't explicitly mention nonce reset protection, it's a common security practice to ensure that only authorized parties can perform updates and that repeated execution of the same update is prevented.
Code Snippet:

uint256 updateNonce = getUpdateNonce(_securityCouncil);
if (_nonce <= updateNonce) {
    emit UpdateNonceTooLow(_securityCouncil, updateNonce, _nonce);
    return false;
}

Tools Used

Manual

Recommended Mitigation Steps

One possible mitigation is to include a separate nonce reset mechanism that can only be triggered by authorized entities, such as DAO administrators. Additionally, the contract could incorporate a unique identifier for each update to prevent replay of previous transactions.

Assessed type

Other

Later added nominees have advantage in case of same votes amount

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L191-L220

Vulnerability details

Impact

Later added nominees have advantage in case of same votes amount

Proof of Concept

In first stage of voting, nominees are added. In case if user received needed amount of support, then he is added to nominee array.
This array extends with each new nominee added.

Then in second stage of elections, 6 members should be elected out of all nominees.
It's done inside SecurityCouncilMemberElectionGovernorCountingUpgradeable.selectTopNominees function.
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L191-L220

    function selectTopNominees(address[] memory nominees, uint240[] memory weights, uint256 k)
        public
        pure
        returns (address[] memory)
    {
        if (nominees.length != weights.length) {
            revert LengthsDontMatch(nominees.length, weights.length);
        }
        if (nominees.length < k) {
            revert NotEnoughNominees(nominees.length, k);
        }


        uint256[] memory topNomineesPacked = new uint256[](k);


        for (uint16 i = 0; i < nominees.length; i++) {
            uint256 packed = (uint256(weights[i]) << 16) | i;


            if (topNomineesPacked[0] < packed) {
                topNomineesPacked[0] = packed;
                LibSort.insertionSort(topNomineesPacked);
            }
        }


        address[] memory topNomineesAddresses = new address[](k);
        for (uint16 i = 0; i < k; i++) {
            topNomineesAddresses[i] = nominees[uint16(topNomineesPacked[i])];
        }


        return topNomineesAddresses;
    }

This function takes all nominees from array and their support weight and then it sorts them in order to get top 6 members.
What is interesting is that in order to sort votes and preserve index of nominee packed variable is stored to be sorted instead of just weight.

Because of that, in case if 2 nominees have same support weight, then the one with biggest i index will be selected as winner, as his packed variable will be bigger.

Tools Used

VsCode

Recommended Mitigation Steps

This is not fair approach, some additional rule should be used to handle such cases.

Assessed type

Error

Analysis

See the markdown file with the details of this report here.

Insecure Module Execution in SecurityCouncilMemberSyncAction Contract

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilMemberSyncAction.sol#L126-L136

Vulnerability details

The SecurityCouncilMemberSyncAction contract contains an insecure module execution vulnerability that deviates from the Constitution of the Arbitrum DAO and introduces a substantial risk to the DAO's security and decision-making process. This vulnerability arises from the improper use of the execTransactionFromModule function without adequate validation or checks, allowing unauthorized and potentially malicious actions to be executed, undermining the democratic governance process and introducing a major security concern.

Impact

The impact of this vulnerability is profound as it jeopardizes the democratic governance structure established by the Constitution of the Arbitrum DAO. Unauthorized or malicious actions performed through this vulnerability could lead to inappropriate updates to the Security Council members, violation of the DAO's principles, and a loss of community trust. The potential outcomes include incorrect election results, unauthorized removals/additions of members, and other actions that are not aligned with the best interests of the DAO.

Proof of Concept

The vulnerable function, _execFromModule, is responsible for executing a transaction via the execTransactionFromModule function from the Gnosis Safe contract. This function lacks proper validation and control mechanisms, which contradicts the principles outlined in the Constitution of the Arbitrum DAO. The vulnerability can be observed in the following code snippet:

    /// @notice Execute provided operation via gnosis safe's trusted execTransactionFromModule entry point
    function _execFromModule(IGnosisSafe securityCouncil, bytes memory data) internal {
        if (
            !securityCouncil.execTransactionFromModule(
                address(securityCouncil), 0, data, OpEnum.Operation.Call
            )
        ) {
            revert ExecFromModuleError({data: data, securityCouncil: address(securityCouncil)});
        }
    }
}

The _execFromModule function is invoked without proper authorization or oversight, potentially enabling a compromised or malicious module to execute unauthorized actions that could harm the interests of the DAO and its members. This deviation from the Constitution's principles undermines the governance process and introduces a significant risk to the ecosystem.

Tools Used

Manual

Recommended Mitigation Steps

Implement a multi-signature approval mechanism: Require multiple authorized parties to review and approve actions before execution. This ensures that critical actions are validated by a consensus of trusted participants.

Assessed type

Access Control

Security councils are not notified about cohorts at initialization

Lines of code

https://github.com/arbitrumfoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L89-#L121

Vulnerability details

Impact

  • Security councils are not notified about cohorts at initialization
  • The worst scenario is that security councils must wait 6-12 months to receive updates about
    cohorts

Proof of Concept

Contract SecurityCouncilManagement has a variable called securityCouncils, documented as The list of Security Councils under management. Any changes to the cohorts in this manager will be pushed to each of these security councils, ensuring that they all stay in sync. The contract uses function _scheduleUpdate to notify each member of securityCouncils about cohort changes, for example when adding a new member using ``:

    function addMember(address _newMember, Cohort _cohort) external onlyRole(MEMBER_ADDER_ROLE) {
        _addMemberToCohortArray(_newMember, _cohort);
        _scheduleUpdate();
        emit MemberAdded(_newMember, _cohort);
    }

 /// @dev Create a union of the second and first cohort, then update all Security Councils under management with that unioned array.
    ///      Updates will need to be scheduled through timelocks and target upgrade executors
    function _scheduleUpdate() internal {
        // always update the nonce
        // this is used to ensure that proposals in the timelocks are unique
        // and calls to the upgradeExecutors are in the correct order
        updateNonce++;
        (address[] memory newMembers, address to, bytes memory data) =
            getScheduleUpdateInnerData(updateNonce);

        ArbitrumTimelock(l2CoreGovTimelock).schedule({
            target: to, // ArbSys address - this will trigger a call from L2->L1
            value: 0,
            // call to ArbSys.sendTxToL1; target the L1 timelock with the calldata previously constucted
            data: data,
            predecessor: bytes32(0),
            // must be unique as the proposal hash is used for replay protection in the L2 timelock
            // we cant be sure another proposal wont use this salt, and the same target + data
            // but in that case the proposal will do what we want it to do anyway
            // this can however block the execution of the election - so in this case the
            // Security Council would need to unblock it by setting the election to executed state
            // in the Member Election governor
            salt: this.generateSalt(newMembers, updateNonce),
            delay: ArbitrumTimelock(l2CoreGovTimelock).getMinDelay()
        });
    }

However, function initialize does not call _scheduleUpdate to notify securityCouncils member about cohorts changes. As a result, securityCouncils will not have information about initialized cohorts members; they must be updated using other methods or wait until an authorized user calls function addMember/removeMember/replaceMember/rotateMember to receive update (these functions all call _scheduleUpdate). The worst case is that the cohorts are initialized and then remains unchanged until the next election, which is 6-12 months according to the constitution.

Furthermore, even though securityCouncils members can receive updates when some authorized user calls addMember/removeMember/replaceMember/rotateMember, these updates only reflect the current cohorts list (not the previous list), so they will never know the cohorts member at the start; some security council members may require a full and exact history of these changes to make decision.

Below is a POC for this issue, save it under governance/test/security-council-mgmt/SecurityCouncilManagerScheduleUpdate.t.sol and run it using command:
forge test --match-path test/security-council-mgmt/SecurityCouncilManagerScheduleUpdate.t.sol -vvvv

In the test, an instance of MockArbitrumTimelock is used as timelock for SecurityCouncilManagement contract, this will emit event CallScheduled whenever function schedule is called; function schedule is only called when _scheduleUpdate is called on SecurityCouncilManagement. The test proves that this event is not emitted at the initialization.

// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.16;

import "forge-std/Test.sol";
import "forge-std/console2.sol";
import "../../src/security-council-mgmt/SecurityCouncilManager.sol";
import "../../src/UpgradeExecRouteBuilder.sol";

import "../util/TestUtil.sol";
import "../util/MockArbSys.sol";
import "../../src/security-council-mgmt/Common.sol";

contract MockArbitrumTimelock {
    event CallScheduled(
        bytes32 indexed id,
        uint256 indexed index,
        address target,
        uint256 value,
        bytes data,
        bytes32 predecessor,
        uint256 delay
    );

    function getMinDelay() external view returns (uint256) {
        return uint256(123);
    }

    function schedule(
        address target,
        uint256 value,
        bytes calldata data,
        bytes32 predecessor,
        bytes32 salt,
        uint256 delay
    ) public virtual {
        emit CallScheduled(salt, 0, target, value, data, predecessor, delay);
    }
}

contract SecurityCouncilManagerTest is Test {
    address[] firstCohort = new address[](6);
    address[6] _firstCohort =
        [address(1111), address(1112), address(1113), address(1114), address(1115), address(1116)];

    address[] secondCohort = new address[](6);
    address[6] _secondCohort =
        [address(2221), address(2222), address(2223), address(2224), address(2225), address(2226)];

    address[] newCohort = new address[](6);
    address[6] _newCohort =
        [address(3331), address(3332), address(3333), address(3334), address(3335), address(3336)];

    address[] newCohortWithADup = new address[](6);
    address dup = address(3335);
    address[6] _newCohortWithADup =
        [address(3331), address(3332), address(3333), address(3334), dup, dup];

    SecurityCouncilManager scm;
    UpgradeExecRouteBuilder uerb;
    address[] memberRemovers = new address[](2);
    address memberRemover1 = address(4444);
    address memberRemover2 = address(4445);

    SecurityCouncilManagerRoles roles = SecurityCouncilManagerRoles({
        admin: address(4441),
        cohortUpdator: address(4442),
        memberAdder: address(4443),
        memberRemovers: memberRemovers,
        memberRotator: address(4446),
        memberReplacer: address(4447)
    });

    address rando = address(6661);

    address memberToAdd = address(7771);

    address l1ArbitrumTimelock = address(8881);

    address payable l2CoreGovTimelock;

    uint256 l1TimelockMinDelay = uint256(1);
    ChainAndUpExecLocation[] chainAndUpExecLocation;
    SecurityCouncilData[] securityCouncils;

    SecurityCouncilData firstSC = SecurityCouncilData({
        securityCouncil: address(9991),
        updateAction: address(9992),
        chainId: 2
    });

    SecurityCouncilData scToAdd = SecurityCouncilData({
        securityCouncil: address(9993),
        updateAction: address(9994),
        chainId: 3
    });

    ChainAndUpExecLocation firstChainAndUpExecLocation = ChainAndUpExecLocation({
        chainId: 2,
        location: UpExecLocation({inbox: address(9993), upgradeExecutor: address(9994)})
    });

    ChainAndUpExecLocation secondChainAndUpExecLocation = ChainAndUpExecLocation({
        chainId: 3,
        location: UpExecLocation({inbox: address(9995), upgradeExecutor: address(9996)})
    });

    address[] bothCohorts;

    function setUp() public {
        chainAndUpExecLocation.push(firstChainAndUpExecLocation);
        chainAndUpExecLocation.push(secondChainAndUpExecLocation);
        uerb = new UpgradeExecRouteBuilder({
            _upgradeExecutors:chainAndUpExecLocation,
            _l1ArbitrumTimelock: l1ArbitrumTimelock,
            _l1TimelockMinDelay: l1TimelockMinDelay
        });
        for (uint256 i = 0; i < 6; i++) {
            secondCohort[i] = _secondCohort[i];
            firstCohort[i] = _firstCohort[i];
            bothCohorts.push(_firstCohort[i]);
            bothCohorts.push(_secondCohort[i]);
            newCohort[i] = _newCohort[i];
            newCohortWithADup[i] = _newCohortWithADup[i];
        }
        address prox = TestUtil.deployProxy(address(new SecurityCouncilManager()));
        scm = SecurityCouncilManager(payable(prox));
        l2CoreGovTimelock = payable(address(new MockArbitrumTimelock()));

        securityCouncils.push(firstSC);
        
    }

    function testSecurityCouncilNotNotifiedAtInitialization() public {
        vm.recordLogs();
        scm.initialize(firstCohort, secondCohort, securityCouncils, roles, l2CoreGovTimelock, uerb);
        Vm.Log[] memory entries = vm.getRecordedLogs();
        assertFalse(
            entries[0].topics[0] == keccak256("CallScheduled(bytes32,uint256,address,uint256,bytes,bytes32,uint256)")
        );

    }
    
    
}

Tools Used

Manual Review

Recommended Mitigation Steps

I recommend calling scheduleUpdate at initialization to notify security councils about cohorts members.

Assessed type

Governance

Unequal cohort length

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L102

Vulnerability details

Impact

During initializing the SecurityCouncilManager, it is possible that two cohorts do not have the same length. This gives the ability to the MEMBER_ADDER_ROLE to add as many member to the second cohort as possible.

Proof of Concept

During initializing SecurityCouncilManager, the cohortSize is set based on the size of firstCohort.
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L102C9-L102C20

But, it does not check whether the secondCohort has the same length as firstCohort or not.

This can be an issue in the following scenario:

Let's say during initializing, the firstCohort has length of 6 and secondCohort has length of 7. So, the cohortSize = 6.

Later, MEMBER_ADDER_ROLE intends to add a member to secondCohort.
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L176C78-L176C95

During adding member to cohort array, it checks whether the to-be-updated cohort (which is here the secondCohort) is full or not.
https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/SecurityCouncilManager.sol#L148

This check always passes for adding member to the secondCohort because cohort.length = 7 and cohortSize = 6.
It means that, MEMBER_ADDER_ROLE can add as many member to secondCohort as possible without any limitation.

Tools Used

Recommended Mitigation Steps

Two solutions:

  1. Adding a check that two cohort have the same length during initialization.
  2. Changing the full cohort check to if (cohort.length >= cohortSize) {

Assessed type

Context

QA Report

See the markdown file with the details of this report here.

SecurityCouncilMemberElectionGovernorCountingUpgradeable will elect members with 0 votes

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L191-L220

Vulnerability details

Impact

SecurityCouncilMemberElectionGovernorCountingUpgradeable will elect members even if they don't have any votes.

Proof of Concept

When nominees are elected, then member election starts. At this point people will vote for the 6 members that will replace previous cohort.

When voting is finished, then securityCouncilManager.replaceCohort is called, with new 6 members.

Function topNominees should go through all nominees and calculate votes that they received. And in the end 6 members should be selected according to the votes amount.

Let's imagine situation, where 10 nominees participated and 5 of them received 0 support.
In this case function topNominees, still will be able to provide 6 members. My concern is that in case if someone received 0 votes, then there is no support for him and he should not be elected. Another thing is that we have 6 persons with 0 votes and the one of them that will win will be selected randomly, which is also wrong.

Tools Used

VsCode

Recommended Mitigation Steps

Do not elect members without support.

Assessed type

Error

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.