Platform: Code4rena
Start Date: 03/08/2023
Pot Size: $90,500 USDC
Total HM: 6
Participants: 36
Period: 7 days
Judge: 0xean
Total Solo HM: 1
Id: 273
League: ETH
Rank: 13/36
Findings: 2
Award: $665.71
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: LeoS
Also found by: 0xAnah, JCK, K42, SAAJ, Sathish9098, Udsen, caventa, dharma09, ernestognw, naman1778, petrichor, rektthecode
38.4497 USDC - $38.45
Possible Optimization =
Here is the optimized code:
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); l2CoreGovTimelock = _l2CoreGovTimelock; _setUpgradeExecRouteBuilder(_router); for (uint256 i = 0; i < _securityCouncils.length; i++) { securityCouncils.push(_securityCouncils[i]); } }
Possible Optimization 1 =
nomineeCount
function. Instead of making these external calls, we could refactor the code to calculate the compliantNomineeCount in the _execute function itself.After Optimization:
function _execute( uint256 proposalId, address[] memory, /* targets */ uint256[] memory, /* values */ bytes[] memory callDatas, bytes32 /*descriptionHash*/ ) internal virtual override { uint256 vettingDeadline = proposalVettingDeadline(proposalId); if (block.number <= vettingDeadline) { revert ProposalInVettingPeriod(block.number, vettingDeadline); } uint256 cnCount = nomineeCount(proposalId) - _elections[proposalId].excludedNomineeCount; uint256 cohortSize = securityCouncilManager.cohortSize(); if (cnCount < cohortSize) { revert InsufficientCompliantNomineeCount(cnCount, cohortSize); } uint256 electionIndex = extractElectionIndex(callDatas); uint256 memberElectionProposalId = securityCouncilMemberElectionGovernor.proposeFromNomineeElectionGovernor(electionIndex); if (memberElectionProposalId != proposalId) { revert ProposalIdMismatch(proposalId, memberElectionProposalId); } }
Possible Optimization 2 =
uint32
, allowing it to be packed into the same slot as the two mapping variables.After:
struct ElectionInfo { mapping(address => bool) isContender; mapping(address => bool) isExcluded; uint32 excludedNomineeCount; }
Possible Optimization =
After Optimization:
function _countVote( uint256 proposalId, address account, uint8 support, uint256 weight, bytes memory params ) internal virtual override { if (support != 1) { revert InvalidSupport(support); } if (params.length != 64) { revert UnexpectedParamsLength(params.length); } // params is encoded as (address contender, uint256 votes) (address contender, uint256 votes) = abi.decode(params, (address, uint256)); if (!isContender(proposalId, contender)) { revert NotEligibleContender(contender); } if (_elections[proposalId].isNominee[contender]) { revert NomineeAlreadyAdded(contender); } // Rest of the function remains the same... }
Possible Optimization 1 =
Here is the optimized code snippet:
function _countVote( uint256 proposalId, address account, uint8 support, uint256 availableVotes, bytes memory params ) internal virtual override { // ... existing code ... ElectionInfo storage election = _elections[proposalId]; uint256 prevVotesUsed = election.votesUsed[account]; if (prevVotesUsed + votes > availableVotes) { revert InsufficientVotes(prevVotesUsed, votes, availableVotes); } uint240 prevWeightReceived = election.weightReceived[nominee]; uint256 newVotesUsed = prevVotesUsed + votes; uint240 newWeightReceived = prevWeightReceived + weight; election.votesUsed[account] = newVotesUsed; election.weightReceived[nominee] = newWeightReceived; // ... existing code ... }
Possible Optimization 2 =
uint240
to accommodate the sorting implementation in selectTopNominees. However, this could be optimized by using uint256
for weightReceived and only casting to uint240
when needed for sorting. This would eliminate the need for the _downCast function and its associated gas costs.Here is the optimized code:
// Change the type of weightReceived to uint256 mapping(address => uint256) weightReceived; // Update the selectTopNominees function to cast weightReceived to uint240 when needed function selectTopNominees(address[] memory nominees, uint256[] memory weights, uint256 k) public pure returns (address[] memory) { // ... existing code ... for (uint16 i = 0; i < nominees.length; i++) { uint256 packed = (uint256(uint240(weights[i])) << 16) | i; // ... existing code ... }
Possible Optimization =
Here is the optimized code:
function propose( address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description ) public override returns (uint256) { // ... existing checks ... ISecurityCouncilManager scm = securityCouncilManager; // Store in memory if (targets[0] != address(scm)) { revert TargetNotManager(targets[0]); } // ... remaining checks ... address memberToRemove = abi.decode(rest, (address)); if ( !scm.firstCohortIncludes(memberToRemove) && !scm.secondCohortIncludes(memberToRemove) ) { revert MemberNotFound(memberToRemove); } // ... remaining code ... }
Possible Optimization =
Here is the optimized code snippet:
address[] public schedTargets; uint256[] public schedValues; bytes[] public schedData; constructor( ChainAndUpExecLocation[] memory _upgradeExecutors, address _l1ArbitrumTimelock, uint256 _l1TimelockMinDelay ) { // ... existing code ... schedTargets = new address[](_upgradeExecutors.length); schedValues = new uint256[](_upgradeExecutors.length); schedData = new bytes[](_upgradeExecutors.length); } function createActionRouteData( uint256[] memory chainIds, address[] memory actionAddresses, uint256[] memory actionValues, bytes[] memory actionDatas, bytes32 predecessor, bytes32 timelockSalt ) public view returns (address, bytes memory) { // ... existing code ... for (uint256 i = 0; i < chainIds.length; i++) { // ... existing code ... schedTargets[i] = // ... existing code ... schedValues[i] = // ... existing code ... schedData[i] = // ... existing code ... } // ... remaining code ... }
Possible Optimization 1 =
getOwners()
function. Currently, the function is called twice in the perform function and once in the getPrevOwner function. You could save gas by calling it once and storing the results in a local array.Here is the optimized code snippet:
function perform(address _securityCouncil, address[] memory _updatedMembers, uint256 _nonce) external returns (bool res) { // ... existing code ... IGnosisSafe securityCouncil = IGnosisSafe(_securityCouncil); // preserve current threshold, the safe ensures that the threshold is never lower than the member count uint256 threshold = securityCouncil.getThreshold(); address[] memory previousOwners = securityCouncil.getOwners(); for (uint256 i = 0; i < _updatedMembers.length; i++) { address member = _updatedMembers[i]; if (!SecurityCouncilMgmtUtils.isInArray(member, previousOwners)) { _addMember(securityCouncil, member, threshold); } } for (uint256 i = 0; i < previousOwners.length; i++) { address owner = previousOwners[i]; if (!SecurityCouncilMgmtUtils.isInArray(owner, _updatedMembers)) { _removeMember(securityCouncil, owner, threshold, previousOwners); } } return true; } function _removeMember(IGnosisSafe securityCouncil, address _member, uint256 _threshold, address[] memory previousOwners) internal { address previousOwner = getPrevOwner(_member, previousOwners); _execFromModule( securityCouncil, abi.encodeWithSelector( IGnosisSafe.removeOwner.selector, previousOwner, _member, _threshold ) ); } function getPrevOwner(address _owner, address[] memory owners) public view returns (address) { // owners are stored as a linked list and removal requires the previous owner address previousOwner = SENTINEL_OWNERS; for (uint256 i = 0; i < owners.length; i++) { address currentOwner = owners[i]; if (currentOwner == _owner) { return previousOwner; } previousOwner = currentOwner; } revert PreviousOwnerNotFound({ targetOwner: _owner, securityCouncil: address(securityCouncil) }); }
getOwners()
function involves an external
call which costs at least 700 gas. This optimization would save approximately 1400 gas.Possible Optimization 2 =
isOwner()
function. Currently, the function is called for each member in the _updatedMembers array. You could save gas by creating a local mapping of owners and checking against it.Here is the optimized code:
function perform(address _securityCouncil, address[] memory _updatedMembers, uint256 _nonce) external returns (bool res) { // ... existing code ... IGnosisSafe securityCouncil = IGnosisSafe(_securityCouncil); // preserve current threshold, the safe ensures that the threshold is never lower than the member count uint256 threshold = securityCouncil.getThreshold(); address[] memory previousOwners = securityCouncil.getOwners(); mapping(address => bool) memory isOwnerMap; for (uint256 i = 0; i < previousOwners.length; i++) { isOwnerMap[previousOwners[i]] = true; } for (uint256 i = 0; i < _updatedMembers.length; i++) { address member = _updatedMembers[i]; if (!isOwnerMap[member]) { _addMember(securityCouncil, member, threshold); } } // ... remaining code ... }
isOwner()
function involves an external
call which costs at least 700 gas. So, if there are n members in the _updatedMembers
array, this optimization would save approximately 700 * n gas.Possible Optimization 1 =
Here is the optimized code snippet:
bool isGovChainEmergencySecurityCouncilContract = Address.isContract(dp.govChainEmergencySecurityCouncil); bool isGovChainProxyAdminContract = Address.isContract(dp.govChainProxyAdmin); bool isL2UpgradeExecutorContract = Address.isContract(dp.l2UpgradeExecutor); bool isArbTokenContract = Address.isContract(dp.arbToken); if (!isGovChainEmergencySecurityCouncilContract) { revert NotAContract(dp.govChainEmergencySecurityCouncil); } if (!isGovChainProxyAdminContract) { revert NotAContract(dp.govChainProxyAdmin); } if (!isL2UpgradeExecutorContract) { revert NotAContract(dp.l2UpgradeExecutor); } if (!isArbTokenContract) { revert NotAContract(dp.arbToken); }
Possible Optimization 2 =
Here is the optimized code:
SecurityCouncilNomineeElectionGovernor.InitParams memory nomineeElectionGovernorParams = SecurityCouncilNomineeElectionGovernor.InitParams({ firstNominationStartDate: dp.firstNominationStartDate, nomineeVettingDuration: dp.nomineeVettingDuration, nomineeVetter: dp.nomineeVetter, securityCouncilManager: securityCouncilManager, securityCouncilMemberElectionGovernor: memberElectionGovernor, token: IVotesUpgradeable(dp.arbToken), owner: dp.l2UpgradeExecutor, quorumNumeratorValue: dp.nomineeQuorumNumerator, votingPeriod: dp.nomineeVotingPeriod }); nomineeElectionGovernor.initialize(nomineeElectionGovernorParams);
Possible Optimization 1 =
Here is the optimized code snippet:
bool prevNonEmergencySecurityCouncilHasProposalRole = l2CoreGovTimelock.hasRole( TIMELOCK_PROPOSAL_ROLE, address(prevNonEmergencySecurityCouncil) ); bool newNonEmergencySecurityCouncilHasProposalRole = l2CoreGovTimelock.hasRole( TIMELOCK_PROPOSAL_ROLE, address(newNonEmergencySecurityCouncil) ); bool securityCouncilManagerHasProposalRole = l2CoreGovTimelock.hasRole( TIMELOCK_PROPOSAL_ROLE, securityCouncilManager ); bool prevEmergencySecurityCouncilHasCancellerRole = l2CoreGovTimelock.hasRole( TIMELOCK_CANCELLER_ROLE, address(prevEmergencySecurityCouncil) ); bool newEmergencySecurityCouncilHasExecutorRole = upgradeExecutor.hasRole( EXECUTOR_ROLE, address(newEmergencySecurityCouncil) ); bool prevEmergencySecurityCouncilHasExecutorRole = upgradeExecutor.hasRole( EXECUTOR_ROLE, address(prevEmergencySecurityCouncil) ); if (!prevNonEmergencySecurityCouncilHasProposalRole) { revert "GovernanceChainSCMgmtActivationAction: prev nonemergency council doesn't have proposal role"; } if (newNonEmergencySecurityCouncilHasProposalRole) { revert "GovernanceChainSCMgmtActivationAction: new nonemergency council already has proposal role"; } if (securityCouncilManagerHasProposalRole) { revert "GovernanceChainSCMgmtActivationAction: securityCouncilManager already has proposal role"; } if (!prevEmergencySecurityCouncilHasCancellerRole) { revert "GovernanceChainSCMgmtActivationAction: prev emergency security council should have cancellor role"; } if (!newEmergencySecurityCouncilHasExecutorRole) { revert "NonGovernanceChainSCMgmtActivationAction: new emergency security council not set"; } if (prevEmergencySecurityCouncilHasExecutorRole) { revert "NonGovernanceChainSCMgmtActivationAction: prev emergency security council still set"; }
Possible Optimization 2 =
Here is the optimized code:
l2CoreGovTimelock.revokeRole(TIMELOCK_PROPOSAL_ROLE, address(prevNonEmergencySecurityCouncil)); l2CoreGovTimelock.grantRole(TIMELOCK_PROPOSAL_ROLE, address(newNonEmergencySecurityCouncil)); l2CoreGovTimelock.grantRole(TIMELOCK_PROPOSAL_ROLE, securityCouncilManager); l2CoreGovTimelock.revokeRole(TIMELOCK_CANCELLER_ROLE, address(prevEmergencySecurityCouncil));
Possible Optimization 3 =
revert
function is called multiple times, which leads to redundant external calls. Using a single revert
function with a dynamic error message can save gas.Here is the optimized code snippet:
string memory errorMessage; if (!prevNonEmergencySecurityCouncilHasProposalRole) { errorMessage = "GovernanceChainSCMgmtActivationAction: prev nonemergency council doesn't have proposal role"; } else if (newNonEmergencySecurityCouncilHasProposalRole) { errorMessage = "GovernanceChainSCMgmtActivationAction: new nonemergency council already has proposal role"; } else if (securityCouncilManagerHasProposalRole) { errorMessage = "GovernanceChainSCMgmtActivationAction: securityCouncilManager already has proposal role"; } else if (!prevEmergencySecurityCouncilHasCancellerRole) { errorMessage = "GovernanceChainSCMgmtActivationAction: prev emergency security council should have cancellor role"; } else if (!newEmergencySecurityCouncilHasExecutorRole) { errorMessage = "NonGovernanceChainSCMgmtActivationAction: new emergency security council not set"; } else if (prevEmergencySecurityCouncilHasExecutorRole) { errorMessage = "NonGovernanceChainSCMgmtActivationAction: prev emergency security council still set"; } if (bytes(errorMessage).length > 0) { revert(errorMessage); }
Estimated gas saved = Calling revert
function involves an external call which costs at least 700 gas. This optimization would save approximately 3500 gas.
#0 - c4-judge
2023-08-18T14:40:42Z
0xean marked the issue as grade-b
🌟 Selected for report: catellatech
Also found by: 0xSmartContract, 0xnev, K42, MSK, Sathish9098, berlin-101, hals, kodyvim, yixxas
The Arbitrum-Security-Council-Elections are a complex and vital part of the Arbitrum network's governance. The above recommendations aim to enhance the system's robustness, efficiency, transparency, and maintainability. By following best practices in contract design, better access control, further gas optimizations, and testing, the system can achieve a high level of security and functionality.
The Arbitrum-Security-Council-Elections are designed to be decentralized, but there are potential centralization risks:
Ensure that the governance process remains decentralized by avoiding concentration of power in a few addresses. Implement a multi-signature mechanism for critical administrative functions.
The election mechanism is well-designed, with clear rules for nominations, voting, and selection. The use of smart contracts ensures transparency and immutability. The nominee vetting process adds an additional layer of security by ensuring that only qualified entities can be elected.
The mechanisms used in the Arbitrum-Security-Council-Elections are robust. The use of the smart contracts ensures transparency and fairness in the election process. The checks and balances in place, such as the nominee vetting process, ensure that only qualified and trustworthy entities become members of the Security Council.
The Arbitrum-Security-Council-Elections codebase is a comprehensive set of contracts that manage the Security Council's nomination, election, and removal processes. The codebase is modular, with each contract serving a specific function, ensuring separation of concerns.
The main contracts involved in the Arbitrum-Security-Council-Elections are:
Each of these contracts interacts cohesively to form the governance system for the Arbitrum Security Council. They ensure a transparent, decentralized, and secure process for nominating, electing, and removing members of the Security Council.
The Arbitrum-Security-Council-Elections are a critical component of the Arbitrum network's governance system. The implementation is well-designed, with a clear and transparent election process. However, there are potential risks and areas of concern that need to be addressed to ensure the integrity and security of the elections. More regular security audits, robust access control, and additional safeguards against potential attack vectors are recommended to enhance the security of the system. Recommendations include further gas optimizations, formal verification, and community engagement to ensure the system's robustness and security, as well as further testing, access control review, optimization, and enhanced documentation.
Overall the Arbitrum-Security-Council-Elections codebase is well-structured and follows best practices in contract development. The modular approach, upgradeable patterns, and clear documentation facilitate understanding and promising future developments.
24 hours
#0 - c4-judge
2023-08-18T23:50:15Z
0xean marked the issue as grade-a