Arbitrum BoLD - hihen's results

A new dispute protocol that unlocks permissionless validation for Arbitrum chains.

General Information

Platform: Code4rena

Start Date: 10/05/2024

Pot Size: $300,500 USDC

Total HM: 4

Participants: 27

Period: 17 days

Judge: Picodes

Total Solo HM: 1

Id: 375

League: ETH

Arbitrum Foundation

Findings Distribution

Researcher Performance

Rank: 16/27

Findings: 1

Award: $0.00

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

0 USDC - $0.00

Labels

bug
grade-b
primary issue
QA (Quality Assurance)
sufficient quality report
Q-09

External Links

QA Report

Summary

Low Issues

Total 91 instances over 13 issues:

IDIssueInstances
[L-01]Passing concat() with dynamic arguments to a hash can cause collisions2
[L-02]Variables shadowing other definitions2
[L-03]Upgradable contracts need a constructor to init and lock the implementation contract3
[L-04]Missing zero address check in initializer1
[L-05]State variables not limited to reasonable values2
[L-06]Contracts are not using their OZ Upgradeable counterparts2
[L-07]Vulnerable versions of packages are being used8
[L-08]Constructor / initialization function lacks parameter validation17
[L-09]Unsafe solidity low-level call can cause gas grief attack1
[L-10]Functions calling contracts/addresses with transfer hooks should be protected by reentrancy guard9
[L-11]Critical functions should be controlled by time locks11
[L-12]Duplicate data may be pushed to the array1
[L-13]Code does not follow the best practice of check-effects-interaction32

Non Critical Issues

Total 1078 instances over 68 issues:

IDIssueInstances
[N-01]Import declarations should import specific identifiers, rather than the whole file153
[N-02]Visibility of state variables is not explicitly defined1
[N-03]Names of private/internal functions should be prefixed with an underscore99
[N-04]Names of private/internal state variables should be prefixed with an underscore10
[N-05]Use of override is unnecessary33
[N-06]Add inline comments for unnamed parameters5
[N-07]Consider providing a ranged getter for array state variables1
[N-08]Consider splitting complex checks into multiple steps1
[N-09]Cast to bytes for clearer semantic meaning1
[N-10]Missing checks for empty bytes when updating bytes state variables4
[N-11]Consider adding a block/deny-list4
[N-12]Constants/Immutables redefined elsewhere4
[N-13]Convert simple if-statements to ternary expressions2
[N-14]Contracts should each be defined in separate files2
[N-15]Contract name does not match its filename3
[N-16]UPPER_CASE names should be reserved for constant/immutable variables5
[N-17]Consider emitting an event at the end of the constructor10
[N-18]Empty function body without comments3
[N-19]Events are emitted without the sender information34
[N-20]Inconsistent floating version pragma4
[N-21]Function state mutability can be restricted to pure2
[N-22]Imports could be organized more systematically28
[N-23]There is no need to initialize bool variables with false1
[N-24]@openzeppelin/contracts should be upgraded to a newer version20
[N-25]Lib @openzeppelin/contracts-upgradeable should be upgraded to a newer version3
[N-26]Expressions for constant values should use immutable rather than constant1
[N-27]Consider moving duplicated strings to constants22
[N-28]Contracts containing only utility functions should be made into libraries3
[N-29]Functions should be named in mixedCase style7
[N-30]Variable names for immutables should use UPPER_CASE_WITH_UNDERSCORES14
[N-31]Names of public/external functions and state variables should NOT be prefixed with underscore1
[N-32]Overridden function has no body2
[N-33]Named imports of parent contracts are missing25
[N-34]Constants should be put on the left side of comparisons106
[N-35]else-block not required12
[N-36]Large multiples of ten should use scientific notation1
[N-37]TODOs left in the code1
[N-38]High cyclomatic complexity1
[N-39]Typos18
[N-40]Consider bounding input array length4
[N-41]Unnecessary casting11
[N-42]Unused contract variables1
[N-43]Unused function parameters2
[N-44]Unused named return2
[N-45]Use delete instead of assigning values to false1
[N-46]Consider using delete rather than assigning zero to clear values3
[N-47]Use the latest Solidity version24
[N-48]Use transfer libraries instead of low level calls to transfer native tokens1
[N-49]Using underscore at the end of variable name27
[N-50]Use a struct to encapsulate multiple function parameters9
[N-51]Returning a struct instead of a bunch of variables is better7
[N-52]Contract variables should have comments11
[N-53]Missing event when a state variables is set in constructor3
[N-54]Empty bytes check is missing5
[N-55]Don't define functions with the same name in a contract7
[N-56]Multiple mappings with same keys can be combined into a single struct mapping for readability7
[N-57]Do not cache immutable variables5
[N-58]Missing event for critical changes17
[N-59]Consider adding emergency-stop functionality6
[N-60]Avoid the use of sensitive terms63
[N-61]Missing checks for uint state variable assignments6
[N-62]Use the Modern Upgradeable Contract Paradigm11
[N-63]Large or complicated code bases should implement invariant tests1
[N-64]The default value is manually set when it is declared23
[N-65]Contracts should have all public/external functions exposed by interfaces9
[N-66]Top-level declarations should be separated by at least two lines120
[N-67]Consider adding formal verification proofs39
[N-68]Use scopes sparingly6

Low Issues

[L-01] Passing concat() with dynamic arguments to a hash can cause collisions

Passing bytes.concat()/string.concat() with multiple dynamic arguments can cause hash collisions. It is recommended to use abi.encode() instead which will pad items to 32 bytes and prevent hash collisions.

There are 2 instances:

717:         return (keccak256(bytes.concat(header, data)), timeBounds);

744:             keccak256(bytes.concat(header, DATA_BLOB_HEADER_FLAG, abi.encodePacked(dataHashes))),

[L-02] Variables shadowing other definitions

A variable declaration shadowing any other existing definition is error-prone. It can cause confusion for developers, potentially introduce bugs, and reduce the readability and maintainability.

There are 2 instances:

/// @audit Shadows `function stakerCount()`
344:         uint64 stakerCount = ROLLUP_READER.stakerCount();

/// @audit Shadows `state variable rollup`
516:         RollupProxy rollup = new RollupProxy{salt: rollupSalt}();

[L-03] Upgradable contracts need a constructor to init and lock the implementation contract

An uninitialized contract can be taken over by an attacker. For an upgradable contract, this applies to both the proxy and its implementation contract, which may impact the proxy. To prevent the implementation contract from being used, we should trigger the initialization in the constructor to automatically lock it when it is deployed. For contracts that inherit Initializable, the _disableInitializers() function is suggested to do this job.

There are 3 instances:

  • RollupAdminLogic.sol ( 15 ):
/// @audit Missing constructor to initialize the implementation contract
15: contract RollupAdminLogic is RollupCore, IRollupAdmin, DoubleLogicUUPSUpgradeable {
  • RollupCore.sol ( 22 ):
/// @audit Missing constructor to initialize the implementation contract
22: abstract contract RollupCore is IRollupCore, PausableUpgradeable {
  • RollupUserLogic.sol ( 15 ):
/// @audit Missing constructor to initialize the implementation contract
15: contract RollupUserLogic is RollupCore, UUPSNotUpgradeable, IRollupUser {

[L-04] Missing zero address check in initializer

Consider adding a zero address check for each address type parameter in initializer.

There is 1 instance:

  • EdgeChallengeManager.sol ( 305-316 ):
/// @audit `_stakeToken not checked`
305:     function initialize(
306:         IAssertionChain _assertionChain,
307:         uint64 _challengePeriodBlocks,
308:         IOneStepProofEntry _oneStepProofEntry,
309:         uint256 layerZeroBlockEdgeHeight,
310:         uint256 layerZeroBigStepEdgeHeight,
311:         uint256 layerZeroSmallStepEdgeHeight,
312:         IERC20 _stakeToken,
313:         address _excessStakeReceiver,
314:         uint8 _numBigStepLevel,
315:         uint256[] calldata _stakeAmounts
316:     ) public initializer {

[L-05] State variables not limited to reasonable values

Consider adding appropriate minimum/maximum value checks to ensure that the following state variables can never be used to excessively harm users, including via griefing.

There are 2 instances:

  • EdgeChallengeManager.sol ( 328-328 ):
328:         challengePeriodBlocks = _challengePeriodBlocks;
  • RollupAdminLogic.sol ( 205-205 ):
205:         minimumAssertionPeriod = newPeriod;

[L-06] Contracts are not using their OZ Upgradeable counterparts

The non-upgradeable standard version of OpenZeppelin’s library are inherited / used by the contracts. It would be safer to use the upgradeable versions of the library contracts to avoid unexpected behavior. Where applicable, use the contracts from @openzeppelin/contracts-upgradeable instead of @openzeppelin/contracts. See this for list of available upgradeable contracts

There are 2 instances:

  • EdgeChallengeManager.sol ( 15 ):
15: import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
  • BOLDUpgradeAction.sol ( 8 ):
8: import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";

[L-07] Vulnerable versions of packages are being used

This project is using specific package versions which are vulnerable to the specific CVEs listed below. Consider switching to more recent versions of these packages that don't have these vulnerabilities.

  • CVE-2023-40014 - MEDIUM - (@openzeppelin/contracts >=4.0.0 <4.9.3): OpenZeppelin Contracts is a library for secure smart contract development. Starting in version 4.0.0 and prior to version 4.9.3, contracts using ERC2771Context along with a custom trusted forwarder may see _msgSender return address(0) in calls that originate from the forwarder with calldata shorter than 20 bytes. This combination of circumstances does not appear to be common, in particular it is not the case for MinimalForwarder from OpenZeppelin Contracts, or any deployed forwarder the team is aware of, given that the signer address is appended to all calls that originate from these forwarders. The problem has been patched in v4.9.3.

  • CVE-2023-40014 - MEDIUM - (@openzeppelin/contracts-upgradeable >=4.0.0 <4.9.3): OpenZeppelin Contracts is a library for secure smart contract development. Starting in version 4.0.0 and prior to version 4.9.3, contracts using ERC2771Context along with a custom trusted forwarder may see _msgSender return address(0) in calls that originate from the forwarder with calldata shorter than 20 bytes. This combination of circumstances does not appear to be common, in particular it is not the case for MinimalForwarder from OpenZeppelin Contracts, or any deployed forwarder the team is aware of, given that the signer address is appended to all calls that originate from these forwarders. The problem has been patched in v4.9.3.

  • CVE-2023-34459 - MEDIUM - (@openzeppelin/contracts >=4.7.0 <4.9.2): OpenZeppelin Contracts is a library for smart contract development. Starting in version 4.7.0 and prior to version 4.9.2, when the verifyMultiProof, verifyMultiProofCalldata, procesprocessMultiProof, or processMultiProofCalldat functions are in use, it is possible to construct merkle trees that allow forging a valid multiproof for an arbitrary set of leaves. A contract may be vulnerable if it uses multiproofs for verification and the merkle tree that is processed includes a node with value 0 at depth 1 (just under the root). This could happen inadvertedly for balanced trees with 3 leaves or less, if the leaves are not hashed. This could happen deliberately if a malicious tree builder includes such a node in the tree. A contract is not vulnerable if it uses single-leaf proving (verify, verifyCalldata, processProof, or processProofCalldata), or if it uses multiproofs with a known tree that has hashed leaves. Standard merkle trees produced or validated with the @openzeppelin/merkle-tree library are safe. The problem has been patched in version 4.9.2. Some workarounds are available. For those using multiproofs: When constructing merkle trees hash the leaves and do not insert empty nodes in your trees. Using the @openzeppelin/merkle-tree package eliminates this issue. Do not accept user-provided merkle roots without reconstructing at least the first level of the tree. Verify the merkle tree structure by reconstructing it from the leaves.

  • CVE-2023-34459 - MEDIUM - (@openzeppelin/contracts-upgradeable >=4.7.0 <4.9.2): OpenZeppelin Contracts is a library for smart contract development. Starting in version 4.7.0 and prior to version 4.9.2, when the verifyMultiProof, verifyMultiProofCalldata, procesprocessMultiProof, or processMultiProofCalldat functions are in use, it is possible to construct merkle trees that allow forging a valid multiproof for an arbitrary set of leaves. A contract may be vulnerable if it uses multiproofs for verification and the merkle tree that is processed includes a node with value 0 at depth 1 (just under the root). This could happen inadvertedly for balanced trees with 3 leaves or less, if the leaves are not hashed. This could happen deliberately if a malicious tree builder includes such a node in the tree. A contract is not vulnerable if it uses single-leaf proving (verify, verifyCalldata, processProof, or processProofCalldata), or if it uses multiproofs with a known tree that has hashed leaves. Standard merkle trees produced or validated with the @openzeppelin/merkle-tree library are safe. The problem has been patched in version 4.9.2. Some workarounds are available. For those using multiproofs: When constructing merkle trees hash the leaves and do not insert empty nodes in your trees. Using the @openzeppelin/merkle-tree package eliminates this issue. Do not accept user-provided merkle roots without reconstructing at least the first level of the tree. Verify the merkle tree structure by reconstructing it from the leaves.

  • CVE-2023-30542 - HIGH - (@openzeppelin/contracts >=4.7.0 <4.8.2): OpenZeppelin Contracts is a library for secure smart contract development. The proposal creation entrypoint (propose) in GovernorCompatibilityBravo allows the creation of proposals with a signatures array shorter than the calldatas array. This causes the additional elements of the latter to be ignored, and if the proposal succeeds the corresponding actions would eventually execute without any calldata. The ProposalCreated event correctly represents what will eventually execute, but the proposal parameters as queried through getActions appear to respect the original intended calldata. This issue has been patched in 4.8.3. As a workaround, ensure that all proposals that pass through governance have equal length signatures and calldatas parameters.

  • CVE-2023-30542 - HIGH - (@openzeppelin/contracts-upgradeable >=4.7.0 <4.8.2): OpenZeppelin Contracts is a library for secure smart contract development. The proposal creation entrypoint (propose) in GovernorCompatibilityBravo allows the creation of proposals with a signatures array shorter than the calldatas array. This causes the additional elements of the latter to be ignored, and if the proposal succeeds the corresponding actions would eventually execute without any calldata. The ProposalCreated event correctly represents what will eventually execute, but the proposal parameters as queried through getActions appear to respect the original intended calldata. This issue has been patched in 4.8.3. As a workaround, ensure that all proposals that pass through governance have equal length signatures and calldatas parameters.

  • CVE-2023-30541 - MEDIUM - (@openzeppelin/contracts >=3.2.0 <4.8.3): OpenZeppelin Contracts is a library for secure smart contract development. A function in the implementation contract may be inaccessible if its selector clashes with one of the proxy's own selectors. Specifically, if the clashing function has a different signature with incompatible ABI encoding, the proxy could revert while attempting to decode the arguments from calldata. The probability of an accidental clash is negligible, but one could be caused deliberately and could cause a reduction in availability. The issue has been fixed in version 4.8.3. As a workaround if a function appears to be inaccessible for this reason, it may be possible to craft the calldata such that ABI decoding does not fail at the proxy and the function is properly proxied through.

  • CVE-2023-30541 - MEDIUM - (@openzeppelin/contracts-upgradeable >=3.2.0 <4.8.3): OpenZeppelin Contracts is a library for secure smart contract development. A function in the implementation contract may be inaccessible if its selector clashes with one of the proxy's own selectors. Specifically, if the clashing function has a different signature with incompatible ABI encoding, the proxy could revert while attempting to decode the arguments from calldata. The probability of an accidental clash is negligible, but one could be caused deliberately and could cause a reduction in availability. The issue has been fixed in version 4.8.3. As a workaround if a function appears to be inaccessible for this reason, it may be possible to craft the calldata such that ABI decoding does not fail at the proxy and the function is properly proxied through.

There are 8 instances:

  • Global finding

[L-08] Constructor / initialization function lacks parameter validation

Constructors and initialization functions play a critical role in contracts by setting important initial states when the contract is first deployed before the system starts. The parameters passed to the constructor and initialization functions directly affect the behavior of the contract / protocol. If incorrect parameters are provided, the system may fail to run, behave abnormally, be unstable, or lack security. Therefore, it's crucial to carefully check each parameter in the constructor and initialization functions. If an exception is found, the transaction should be rolled back.

<details> <summary>There are 17 instances (click to show):</summary>
  • AssertionStakingPool.sol ( 31-34 ):
/// @audit `_assertionHash`
31:     constructor(
32:         address _rollup,
33:         bytes32 _assertionHash
34:     ) AbsBoldStakingPool(IRollupCore(_rollup).stakeToken()) {
  • EdgeStakingPool.sol ( 35-38 ):
/// @audit `_edgeId`
35:     constructor(
36:         address _challengeManager,
37:         bytes32 _edgeId
38:     ) AbsBoldStakingPool(address(EdgeChallengeManager(_challengeManager).stakeToken())) {
/// @audit `_maxDataSize`
/// @audit `_isUsingFeeToken`
/// @audit `_isDelayBufferable`
140:     constructor(
141:         uint256 _maxDataSize,
142:         IReader4844 reader4844_,
143:         bool _isUsingFeeToken,
144:         bool _isDelayBufferable
145:     ) {

/// @audit `maxTimeVariation_`
/// @audit `bufferConfig_`
177:     function initialize(
178:         IBridge bridge_,
179:         ISequencerInbox.MaxTimeVariation calldata maxTimeVariation_,
180:         BufferConfig memory bufferConfig_
181:     ) external onlyDelegated {
  • EdgeChallengeManager.sol ( 305-316 ):
/// @audit `layerZeroBlockEdgeHeight`
/// @audit `layerZeroBigStepEdgeHeight`
/// @audit `layerZeroSmallStepEdgeHeight`
305:     function initialize(
306:         IAssertionChain _assertionChain,
307:         uint64 _challengePeriodBlocks,
308:         IOneStepProofEntry _oneStepProofEntry,
309:         uint256 layerZeroBlockEdgeHeight,
310:         uint256 layerZeroBigStepEdgeHeight,
311:         uint256 layerZeroSmallStepEdgeHeight,
312:         IERC20 _stakeToken,
313:         address _excessStakeReceiver,
314:         uint8 _numBigStepLevel,
315:         uint256[] calldata _stakeAmounts
316:     ) public initializer {
/// @audit `__array`
169:     constructor(uint256[] memory __array) {

/// @audit `contracts`
/// @audit `proxyAdmins`
/// @audit `implementations`
/// @audit `settings`
287:     constructor(
288:         Contracts memory contracts,
289:         ProxyAdmins memory proxyAdmins,
290:         Implementations memory implementations,
291:         Settings memory settings
292:     ) {
  • BridgeCreator.sol ( 45-48 ):
/// @audit `_ethBasedTemplates`
/// @audit `_erc20BasedTemplates`
45:     constructor(
46:         BridgeTemplates memory _ethBasedTemplates,
47:         BridgeTemplates memory _erc20BasedTemplates
48:     ) Ownable() {
</details>

[L-09] Unsafe solidity low-level call can cause gas grief attack

Using the low-level calls of a solidity address can leave the contract open to gas grief attacks. These attacks occur when the called contract returns a large amount of data. So when calling an external contract, it is necessary to check the length of the return data before reading/copying it (using returndatasize()).

There is 1 instance:

304:             (bool sent, ) = msg.sender.call{value: address(this).balance}("");

[L-10] Functions calling contracts/addresses with transfer hooks should be protected by reentrancy guard

Even if the function follows the best practice of check-effects-interaction, not using a reentrancy guard when there may be transfer hooks opens the users of this protocol up to read-only reentrancy vulnerability with no way to protect them except by block-listing the entire protocol.

<details> <summary>There are 9 instances (click to show):</summary>
35:         IERC20(stakeToken).safeTransferFrom(msg.sender, address(this), amount);

51:         IERC20(stakeToken).safeTransfer(msg.sender, amount);
434:             st.safeTransferFrom(msg.sender, receiver, sa);

581:             st.safeTransfer(edge.staker, sa);
312:             IERC20(_nativeToken).safeTransferFrom(msg.sender, _inbox, totalFee);
215:             IERC20(stakeToken).safeTransfer(loserStakeEscrow, assertion.beforeStateData.configData.requiredStake);

295:                 IERC20(stakeToken).safeTransfer(loserStakeEscrow, assertion.beforeStateData.configData.requiredStake);

362:         IERC20(stakeToken).safeTransfer(msg.sender, amount);

367:         IERC20(stakeToken).safeTransferFrom(msg.sender, address(this), tokenAmount);
</details>

[L-11] Critical functions should be controlled by time locks

It is a good practice to give time for users to react and adjust to critical changes. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate (less risk of a malicious owner making a sandwich attack on a user).

<details> <summary>There are 11 instances (click to show):</summary>
894:     function setIsBatchPoster(address addr, bool isBatchPoster_)
895:         external
896:         onlyRollupOwnerOrBatchPosterManager
897:     {

933:     function setIsSequencer(address addr, bool isSequencer_)
934:         external
935:         onlyRollupOwnerOrBatchPosterManager
936:     {

942:     function setBatchPosterManager(address newBatchPosterManager) external onlyRollupOwner {
  • BOLDUpgradeAction.sol ( 411-411 ):
411:     function upgradeSurroundingContracts(address newRollupAddress) private {
53:     function updateTemplates(BridgeTemplates calldata _newTemplates) external onlyOwner {

58:     function updateERC20Templates(BridgeTemplates calldata _newTemplates) external onlyOwner {
  • RollupAdminLogic.sol ( 195-195 ):
195:     function setOwner(address newOwner) external override {
355:     function deleteStaker(address stakerAddress) private {
  • RollupCreator.sol ( 63-72 ):
63:     function setTemplates(
64:         BridgeCreator _bridgeCreator,
65:         IOneStepProofEntry _osp,
66:         IEdgeChallengeManager _challengeManagerLogic,
67:         IRollupAdmin _rollupAdminLogic,
68:         IRollupUser _rollupUserLogic,
69:         IUpgradeExecutor _upgradeExecutorLogic,
70:         address _validatorWalletCreator,
71:         DeployHelper _l2FactoriesDeployer
72:     ) external onlyOwner {
232:     function _addToDeposit(address stakerAddress, uint256 depositAmount) internal onlyValidator whenNotPaused {

349:     function addToDeposit(address stakerAddress, uint256 tokenAmount) external onlyValidator whenNotPaused {
</details>

[L-12] Duplicate data may be pushed to the array

When data is pushed into the array, it does not overwrite or remove the existing identical elements. Duplicate data added to the state array can cause problems such as operations being repeated incorrectly, assets being assigned repeatedly or weights being calculated repeatedly.

There is 1 instance:

/// @audit createNewStake()
276:         _stakerList.push(stakerAddress);

[L-13] Code does not follow the best practice of check-effects-interaction

Code should follow the best-practice of check-effects-interaction, where state variables are updated before any external calls are made. Doing so prevents a large class of reentrancy bugs.

<details> <summary>There are 32 instances (click to show):</summary>
/// @audit `stakeToken()` is called on line 38
39:         challengeManager = _challengeManager;

/// @audit `stakeToken()` is called on line 38
40:         edgeId = _edgeId;
/// @audit `delayedMessageCount()` is called on line 807
816:         totalDelayedMessagesRead = afterDelayedMessagesRead;
/// @audit `pause()` is called on line 342
348:             stakerCount = 50;

/// @audit `pause()` is called on line 342
350:         for (uint64 i = 0; i < stakerCount; i++) {

/// @audit `pause()` is called on line 342
355:                 stakersToRefund[0] = stakerAddr;

/// @audit `cleanupOldRollup()` is called on line 466, triggering an external call - `pause()`
522:         config.owner = address(this);

/// @audit `cleanupOldRollup()` is called on line 466, triggering an external call - `pause()`
528:             for (uint256 i = 0; i < validators.length; i++) {

/// @audit `cleanupOldRollup()` is called on line 466, triggering an external call - `pause()`
530:                 _vals[i] = true;
/// @audit `setDelayedInbox()` is called on line 26
29:         inbox = connectedContracts.inbox;

/// @audit `setDelayedInbox()` is called on line 26
30:         outbox = connectedContracts.outbox;

/// @audit `setDelayedInbox()` is called on line 26
32:         rollupEventInbox = connectedContracts.rollupEventInbox;

/// @audit `setDelayedInbox()` is called on line 26
44:         validatorWalletCreator = connectedContracts.validatorWalletCreator;

/// @audit `setDelayedInbox()` is called on line 26
45:         challengeManager = connectedContracts.challengeManager;

/// @audit `setDelayedInbox()` is called on line 26
47:         confirmPeriodBlocks = config.confirmPeriodBlocks;

/// @audit `setDelayedInbox()` is called on line 26
48:         chainId = config.chainId;

/// @audit `setDelayedInbox()` is called on line 26
49:         baseStake = config.baseStake;

/// @audit `setDelayedInbox()` is called on line 26
50:         wasmModuleRoot = config.wasmModuleRoot;

/// @audit `setDelayedInbox()` is called on line 26
52:         minimumAssertionPeriod = 75;

/// @audit `setDelayedInbox()` is called on line 26
53:         challengeGracePeriodBlocks = config.challengeGracePeriodBlocks;

/// @audit `setDelayedInbox()` is called on line 26
58:         loserStakeEscrow = config.loserStakeEscrow;

/// @audit `setDelayedInbox()` is called on line 26
60:         stakeToken = config.stakeToken;

/// @audit `setDelayedInbox()` is called on line 26
61:         anyTrustFastConfirmer = config.anyTrustFastConfirmer;

/// @audit `setDelayedInbox()` is called on line 26
74:             currentInboxCount += 1;

/// @audit `setDelayedInbox()` is called on line 26
89:         assertionInputs.afterState = config.genesisAssertionState;

/// @audit `setDelayedInbox()` is called on line 26
102:             _assertionCreatedAtArbSysBlock[genesisHash] = ArbSys(address(100)).arbBlockNumber();
/// @audit `updateSendRoot()` is called on line 261
263:         _latestConfirmed = assertionHash;

/// @audit `updateSendRoot()` is called on line 261
264:         assertion.status = AssertionStatus.Confirmed;
/// @audit `()` is called on line 188
208:         deployParams.config.owner = address(this);

/// @audit `()` is called on line 188
225:         for (uint256 i = 0; i < deployParams.batchPosters.length; i++) {

/// @audit `()` is called on line 188
235:             for (uint256 i = 0; i < deployParams.validators.length; i++) {

/// @audit `()` is called on line 188
236:                 _vals[i] = true;
</details>

Non Critical Issues

[N-01] Import declarations should import specific identifiers, rather than the whole file

Using import declarations of the form import {<identifier_name>} from "some/file.sol" avoids polluting the symbol namespace making flattened files smaller, and speeds up compilation (but does not save any gas).

<details> <summary>There are 153 instances (click to show):</summary>
  • AssertionStakingPool.sol ( 8, 9, 10, 11 ):
8: import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

9: import "../rollup/IRollupLogic.sol";

10: import "./AbsBoldStakingPool.sol";

11: import "./interfaces/IAssertionStakingPool.sol";
  • AssertionStakingPoolCreator.sol ( 8, 9, 10 ):
8: import "./AssertionStakingPool.sol";

9: import "./StakingPoolCreatorUtils.sol";

10: import "./interfaces/IAssertionStakingPoolCreator.sol";
  • EdgeStakingPool.sol ( 7, 8, 9, 11, 12 ):
7: import "./AbsBoldStakingPool.sol";

8: import "./interfaces/IEdgeStakingPool.sol";

9: import "../challengeV2/EdgeChallengeManager.sol";

11: import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

12: import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
  • EdgeStakingPoolCreator.sol ( 8, 9, 10 ):
8: import "./EdgeStakingPool.sol";

9: import "./StakingPoolCreatorUtils.sol";

10: import "./interfaces/IEdgeStakingPoolCreator.sol";
  • StakingPoolCreatorUtils.sol ( 8, 9 ):
8: import "@openzeppelin/contracts/utils/Create2.sol";

9: import "@openzeppelin/contracts/utils/Address.sol";
  • IAssertionStakingPool.sol ( 7, 8 ):
7: import "../../rollup/IRollupLogic.sol";

8: import "./IAbsBoldStakingPool.sol";
  • IAssertionStakingPoolCreator.sol ( 7, 8 ):
7: import "../../rollup/IRollupLogic.sol";

8: import "./IAssertionStakingPool.sol";
  • IEdgeStakingPool.sol ( 7, 8 ):
7: import "../../challengeV2/EdgeChallengeManager.sol";

8: import "./IAbsBoldStakingPool.sol";
  • IEdgeStakingPoolCreator.sol ( 7 ):
7: import "./IEdgeStakingPool.sol";
  • DelayBuffer.sol ( 7, 8 ):
7: import "./Messages.sol";

8: import "./DelayBufferTypes.sol";
  • DelayBufferTypes.sol ( 5 ):
5: import "./Messages.sol";
9: import "../libraries/IGasRefunder.sol";

10: import "./IDelayedMessageProvider.sol";

11: import "./IBridge.sol";

12: import "./Messages.sol";

13: import "./DelayBufferTypes.sol";
40: import "./IBridge.sol";

41: import "./IInboxBase.sol";

42: import "./ISequencerInbox.sol";

43: import "../rollup/IRollupLogic.sol";

44: import "./Messages.sol";

45: import "../precompiles/ArbGasInfo.sol";

46: import "../precompiles/ArbSys.sol";

47: import "../libraries/IReader4844.sol";

50: import "../libraries/DelegateCallAware.sol";

53: import "../libraries/ArbitrumChecker.sol";

55: import "./DelayBuffer.sol";
7: import "../rollup/Assertion.sol";

8: import "./libraries/UintUtilsLib.sol";

9: import "./IAssertionChain.sol";

10: import "./libraries/EdgeChallengeManagerLib.sol";

11: import "../libraries/Constants.sol";

12: import "../state/Machine.sol";

14: import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

15: import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
  • IAssertionChain.sol ( 7, 8, 9 ):
7: import "../bridge/IBridge.sol";

8: import "../osp/IOneStepProofEntry.sol";

9: import "../rollup/Assertion.sol";
  • ChallengeEdgeLib.sol ( 7, 8 ):
7: import "./Enums.sol";

8: import "./ChallengeErrors.sol";
  • ChallengeErrors.sol ( 7 ):
7: import "./Enums.sol";
  • EdgeChallengeManagerLib.sol ( 7, 8, 9, 10, 11, 12, 13 ):
7: import "./UintUtilsLib.sol";

8: import "./MerkleTreeLib.sol";

9: import "./ChallengeEdgeLib.sol";

10: import "../../osp/IOneStepProofEntry.sol";

11: import "../../rollup/AssertionState.sol";

12: import "../../libraries/Constants.sol";

13: import "./ChallengeErrors.sol";
  • MerkleTreeLib.sol ( 7, 8, 9 ):
7: import "../../libraries/MerkleLib.sol";

8: import "./ArrayUtilsLib.sol";

9: import "./UintUtilsLib.sol";
  • Assertion.sol ( 7 ):
7: import "./AssertionState.sol";
  • AssertionState.sol ( 7, 8, 9 ):
7: import "../state/GlobalState.sol";

8: import "../state/Machine.sol";

9: import "../osp/IOneStepProofEntry.sol";
  • BOLDUpgradeAction.sol ( 7, 8, 9, 10, 11 ):
7: import "@openzeppelin/contracts-upgradeable/utils/Create2Upgradeable.sol";

8: import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";

9: import "./RollupProxy.sol";

10: import "./RollupLib.sol";

11: import "./RollupAdminLogic.sol";
7: import "../bridge/Bridge.sol";

8: import "../bridge/SequencerInbox.sol";

9: import "../bridge/Inbox.sol";

10: import "../bridge/Outbox.sol";

11: import "./RollupEventInbox.sol";

12: import "../bridge/ERC20Bridge.sol";

13: import "../bridge/ERC20Inbox.sol";

14: import "../rollup/ERC20RollupEventInbox.sol";

15: import "../bridge/ERC20Outbox.sol";

17: import "../bridge/IBridge.sol";

18: import "@openzeppelin/contracts/access/Ownable.sol";

19: import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
7: import "../state/GlobalState.sol";

8: import "../state/Machine.sol";

9: import "../bridge/ISequencerInbox.sol";

10: import "../bridge/IBridge.sol";

11: import "../bridge/IOutbox.sol";

12: import "../bridge/IInboxBase.sol";

13: import "./IRollupEventInbox.sol";

14: import "./IRollupLogic.sol";

15: import "../challengeV2/EdgeChallengeManager.sol";
  • IRollupAdmin.sol ( 7, 8, 9, 10, 11 ):
7: import "./IRollupCore.sol";

8: import "../bridge/ISequencerInbox.sol";

9: import "../bridge/IOutbox.sol";

10: import "../bridge/IOwnable.sol";

11: import "./Config.sol";
7: import "./Assertion.sol";

8: import "../bridge/IBridge.sol";

9: import "../bridge/IOutbox.sol";

10: import "../bridge/IInboxBase.sol";

11: import "./IRollupEventInbox.sol";

12: import "../challengeV2/EdgeChallengeManager.sol";

13: import "../challengeV2/IAssertionChain.sol";
  • IRollupLogic.sol ( 7, 8, 9, 10 ):
7: import "./IRollupCore.sol";

8: import "../bridge/ISequencerInbox.sol";

9: import "../bridge/IOutbox.sol";

10: import "../bridge/IOwnable.sol";
7: import "./IRollupAdmin.sol";

8: import "./IRollupLogic.sol";

9: import "./RollupCore.sol";

10: import "../bridge/IOutbox.sol";

11: import "../bridge/ISequencerInbox.sol";

12: import "../libraries/DoubleLogicUUPSUpgradeable.sol";

13: import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
7: import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";

9: import "./Assertion.sol";

10: import "./RollupLib.sol";

11: import "./IRollupEventInbox.sol";

12: import "./IRollupCore.sol";

14: import "../state/Machine.sol";

16: import "../bridge/ISequencerInbox.sol";

17: import "../bridge/IBridge.sol";

18: import "../bridge/IOutbox.sol";

19: import "../challengeV2/EdgeChallengeManager.sol";

20: import "../libraries/ArbitrumChecker.sol";
7: import "./RollupProxy.sol";

8: import "./IRollupAdmin.sol";

9: import "./BridgeCreator.sol";

10: import "@offchainlabs/upgrade-executor/src/IUpgradeExecutor.sol";

11: import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";

12: import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

13: import "@openzeppelin/contracts/access/Ownable.sol";
7: import "../state/GlobalState.sol";

8: import "../bridge/ISequencerInbox.sol";

10: import "../bridge/IBridge.sol";

11: import "../bridge/IOutbox.sol";

12: import "../bridge/IInboxBase.sol";

13: import "./Assertion.sol";

14: import "./IRollupEventInbox.sol";

15: import "../challengeV2/EdgeChallengeManager.sol";
  • RollupProxy.sol ( 7, 8, 9 ):
7: import "../libraries/AdminFallbackProxy.sol";

8: import "./IRollupAdmin.sol";

9: import "./Config.sol";
  • RollupUserLogic.sol ( 7, 10, 11, 12 ):
7: import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

10: import "../libraries/UUPSNotUpgradeable.sol";

11: import "./RollupCore.sol";

12: import "./IRollupLogic.sol";
</details>

[N-02] Visibility of state variables is not explicitly defined

To avoid misunderstandings and unexpected state accesses, it is recommended to explicitly define the visibility of each state variable.

There is 1 instance:

  • BOLDUpgradeAction.sol ( 167-167 ):
167:     uint256[] _array;

[N-03] Names of private/internal functions should be prefixed with an underscore

It is recommended by the Solidity Style Guide.

<details> <summary>There are 99 instances (click to show):</summary>
  • StakingPoolCreatorUtils.sol ( 13-13 ):
13:     function getPool(bytes memory creationCode, bytes memory args) internal view returns (address) {
33:     function calcBuffer(
34:         uint256 start,
35:         uint256 end,
36:         uint256 buffer,
37:         uint256 sequenced,
38:         uint256 threshold,
39:         uint256 max,
40:         uint256 replenishRateInBasis
41:     ) internal pure returns (uint256) {

68:     function update(BufferData storage self, uint64 blockNumber) internal {

82:     function calcPendingBuffer(BufferData storage self, uint64 blockNumber)
83:         internal
84:         view
85:         returns (uint64)

104:     function isSynced(BufferData storage self) internal view returns (bool) {

108:     function isUpdatable(BufferData storage self) internal view returns (bool) {

115:     function isValidBufferConfig(BufferConfig memory config) internal pure returns (bool) {
216:     function getTimeBounds() internal view virtual returns (IBridge.TimeBounds memory) {

269:     function maxTimeVariationInternal()
270:         internal
271:         view
272:         returns (
273:             uint64,
274:             uint64,
275:             uint64,
276:             uint64
277:         )

455:     function addSequencerL2BatchFromBlobsImpl(
456:         uint256 sequenceNumber,
457:         uint256 afterDelayedMessagesRead,
458:         uint256 prevMessageCount,
459:         uint256 newMessageCount
460:     ) internal {

512:     function addSequencerL2BatchFromCalldataImpl(
513:         uint256 sequenceNumber,
514:         bytes calldata data,
515:         uint256 afterDelayedMessagesRead,
516:         uint256 prevMessageCount,
517:         uint256 newMessageCount,
518:         bool isFromOrigin
519:     ) internal {

605:     function delayProofImpl(uint256 afterDelayedMessagesRead, DelayProof memory delayProof)
606:         internal
607:     {

628:     function isDelayProofRequired(uint256 afterDelayedMessagesRead) internal view returns (bool) {

637:     function packHeader(uint256 afterDelayedMessagesRead)
638:         internal
639:         view
640:         returns (bytes memory, IBridge.TimeBounds memory)

659:     function formEmptyDataHash(uint256 afterDelayedMessagesRead)
660:         internal
661:         view
662:         returns (bytes32, IBridge.TimeBounds memory)

675:     function isValidCallDataFlag(bytes1 headerByte) internal pure returns (bool) {

688:     function formCallDataHash(bytes calldata data, uint256 afterDelayedMessagesRead)
689:         internal
690:         view
691:         returns (bytes32, IBridge.TimeBounds memory)

725:     function formBlobDataHash(uint256 afterDelayedMessagesRead)
726:         internal
727:         view
728:         virtual
729:         returns (
730:             bytes32,
731:             IBridge.TimeBounds memory,
732:             uint256
733:         )

755:     function submitBatchSpendingReport(
756:         bytes32 dataHash,
757:         uint256 seqMessageIndex,
758:         uint256 gasPrice,
759:         uint256 extraGas
760:     ) internal {

791:     function addSequencerL2BatchImpl(
792:         bytes32 dataHash,
793:         uint256 afterDelayedMessagesRead,
794:         uint256 calldataLengthPosted,
795:         uint256 prevMessageCount,
796:         uint256 newMessageCount
797:     )
798:         internal
799:         returns (
800:             uint256 seqMessageIndex,
801:             bytes32 beforeAcc,
802:             bytes32 delayedAcc,
803:             bytes32 acc
804:         )

843:     function delayBufferableBlocks(uint64 _buffer) internal view returns (uint64) {
13:     function append(bytes32[] memory arr, bytes32 newItem) internal pure returns (bytes32[] memory) {

27:     function slice(bytes32[] memory arr, uint256 startIndex, uint256 endIndex)
28:         internal
29:         pure
30:         returns (bytes32[] memory)

45:     function concat(bytes32[] memory arr1, bytes32[] memory arr2) internal pure returns (bytes32[] memory) {
69:     function newEdgeChecks(
70:         bytes32 originId,
71:         bytes32 startHistoryRoot,
72:         uint256 startHeight,
73:         bytes32 endHistoryRoot,
74:         uint256 endHeight
75:     ) internal pure {

93:     function newLayerZeroEdge(
94:         bytes32 originId,
95:         bytes32 startHistoryRoot,
96:         uint256 startHeight,
97:         bytes32 endHistoryRoot,
98:         uint256 endHeight,
99:         bytes32 claimId,
100:         address staker,
101:         uint8 level
102:     ) internal view returns (ChallengeEdge memory) {

133:     function newChildEdge(
134:         bytes32 originId,
135:         bytes32 startHistoryRoot,
136:         uint256 startHeight,
137:         bytes32 endHistoryRoot,
138:         uint256 endHeight,
139:         uint8 level
140:     ) internal view returns (ChallengeEdge memory) {

166:     function mutualIdComponent(
167:         uint8 level,
168:         bytes32 originId,
169:         uint256 startHeight,
170:         bytes32 startHistoryRoot,
171:         uint256 endHeight
172:     ) internal pure returns (bytes32) {

180:     function mutualId(ChallengeEdge storage ce) internal view returns (bytes32) {

184:     function mutualIdMem(ChallengeEdge memory ce) internal pure returns (bytes32) {

189:     function idComponent(
190:         uint8 level,
191:         bytes32 originId,
192:         uint256 startHeight,
193:         bytes32 startHistoryRoot,
194:         uint256 endHeight,
195:         bytes32 endHistoryRoot
196:     ) internal pure returns (bytes32) {

208:     function idMem(ChallengeEdge memory edge) internal pure returns (bytes32) {

215:     function id(ChallengeEdge storage edge) internal view returns (bytes32) {

222:     function exists(ChallengeEdge storage edge) internal view returns (bool) {

228:     function length(ChallengeEdge storage edge) internal view returns (uint256) {

239:     function setChildren(ChallengeEdge storage edge, bytes32 lowerChildId, bytes32 upperChildId) internal {

249:     function setConfirmed(ChallengeEdge storage edge) internal {

258:     function isLayerZero(ChallengeEdge storage edge) internal view returns (bool) {

264:     function setRefunded(ChallengeEdge storage edge) internal {

279:     function levelToType(uint8 level, uint8 numBigStepLevels) internal pure returns (EdgeType eType) {
141:     function get(EdgeStore storage store, bytes32 edgeId) internal view returns (ChallengeEdge storage) {

152:     function getNoCheck(EdgeStore storage store, bytes32 edgeId) internal view returns (ChallengeEdge storage) {

160:     function add(EdgeStore storage store, ChallengeEdge memory edge) internal returns (EdgeAddedData memory) {

213:     function layerZeroTypeSpecificChecks(
214:         EdgeStore storage store,
215:         CreateEdgeArgs calldata args,
216:         AssertionReferenceData memory ard,
217:         IOneStepProofEntry oneStepProofEntry,
218:         uint8 numBigStepLevel
219:     ) private view returns (ProofData memory, bytes32) {

327:     function isPowerOfTwo(uint256 x) internal pure returns (bool) {

344:     function layerZeroCommonChecks(ProofData memory proofData, CreateEdgeArgs calldata args, uint256 expectedEndHeight)
345:         private
346:         pure
347:         returns (bytes32)

391:     function toLayerZeroEdge(bytes32 originId, bytes32 startHistoryRoot, CreateEdgeArgs calldata args)
392:         private
393:         view
394:         returns (ChallengeEdge memory)

411:     function createLayerZeroEdge(
412:         EdgeStore storage store,
413:         CreateEdgeArgs calldata args,
414:         AssertionReferenceData memory ard,
415:         IOneStepProofEntry oneStepProofEntry,
416:         uint256 expectedEndHeight,
417:         uint8 numBigStepLevel,
418:         bool whitelistEnabled
419:     ) internal returns (EdgeAddedData memory) {

443:     function getPrevAssertionHash(EdgeStore storage store, bytes32 edgeId) internal view returns (bytes32) {

463:     function hasRival(EdgeStore storage store, bytes32 edgeId) internal view returns (bool) {

483:     function hasLengthOneRival(EdgeStore storage store, bytes32 edgeId) internal view returns (bool) {

488:     function timeUnrivaledTotal(EdgeStore storage store, bytes32 edgeId) internal view returns (uint256) {

502:     function updateTimerCache(EdgeStore storage store, bytes32 edgeId, uint256 newValue)
503:         internal
504:         returns (bool, uint256)

516:     function updateTimerCacheByChildren(EdgeStore storage store, bytes32 edgeId) internal returns (bool, uint256) {

520:     function updateTimerCacheByClaim(
521:         EdgeStore storage store,
522:         bytes32 edgeId,
523:         bytes32 claimingEdgeId,
524:         uint8 numBigStepLevel
525:     ) internal returns (bool, uint256) {

537:     function timeUnrivaled(EdgeStore storage store, bytes32 edgeId) internal view returns (uint256) {

576:     function mandatoryBisectionHeight(uint256 start, uint256 end) internal pure returns (uint256) {

604:     function bisectEdge(EdgeStore storage store, bytes32 edgeId, bytes32 bisectionHistoryRoot, bytes memory prefixProof)
605:         internal
606:         returns (bytes32, EdgeAddedData memory, EdgeAddedData memory)

662:     function setConfirmedRival(EdgeStore storage store, bytes32 edgeId) internal {

674:     function nextEdgeLevel(uint8 level, uint8 numBigStepLevel) internal pure returns (uint8) {

689:     function checkClaimIdLink(EdgeStore storage store, bytes32 edgeId, bytes32 claimingEdgeId, uint8 numBigStepLevel)
690:         private
691:         view
692:     {

723:     function confirmEdgeByTime(
724:         EdgeStore storage store,
725:         bytes32 edgeId,
726:         uint64 claimedAssertionUnrivaledBlocks,
727:         uint64 confirmationThresholdBlock
728:     ) internal returns (uint256) {

766:     function confirmEdgeByOneStepProof(
767:         EdgeStore storage store,
768:         bytes32 edgeId,
769:         IOneStepProofEntry oneStepProofEntry,
770:         OneStepData calldata oneStepData,
771:         ExecutionContext memory execCtx,
772:         bytes32[] calldata beforeHistoryInclusionProof,
773:         bytes32[] calldata afterHistoryInclusionProof,
774:         uint8 numBigStepLevel,
775:         uint256 bigStepHeight,
776:         uint256 smallStepHeight
777:     ) internal {
110:     function root(bytes32[] memory me) internal pure returns (bytes32) {

151:     function appendCompleteSubTree(bytes32[] memory me, uint256 level, bytes32 subtreeRoot)
152:         internal
153:         pure
154:         returns (bytes32[] memory)

238:     function appendLeaf(bytes32[] memory me, bytes32 leaf) internal pure returns (bytes32[] memory) {

251:     function maximumAppendBetween(uint256 startSize, uint256 endSize) internal pure returns (uint256) {

292:     function treeSize(bytes32[] memory me) internal pure returns (uint256) {

312:     function verifyPrefixProof(
313:         bytes32 preRoot,
314:         uint256 preSize,
315:         bytes32 postRoot,
316:         uint256 postSize,
317:         bytes32[] memory preExpansion,
318:         bytes32[] memory proof
319:     ) internal pure {

359:     function verifyInclusionProof(bytes32 rootHash, bytes32 leaf, uint256 index, bytes32[] memory proof)
360:         internal
361:         pure
362:     {
14:     function leastSignificantBit(uint256 x) internal pure returns (uint256 msb) {

29:     function mostSignificantBit(uint256 x) internal pure returns (uint256 msb) {
70:     function createAssertion(
71:         bool _isFirstChild,
72:         bytes32 _configHash
73:     ) internal view returns (AssertionNode memory) {

85:     function childCreated(AssertionNode storage self) internal {

93:     function requireExists(AssertionNode memory self) internal pure {
18:     function toExecutionState(AssertionState memory state) internal pure returns (ExecutionState memory) {

22:     function hash(AssertionState memory state) internal pure returns (bytes32) {
341:     function cleanupOldRollup() private {

367:     function createConfig() private view returns (Config memory) {

411:     function upgradeSurroundingContracts(address newRollupAddress) private {

433:     function upgradeSequencerInbox() private {
126:     function getAssertionStorage(bytes32 assertionHash) internal view returns (AssertionNode storage) {

225:     function initializeCore(AssertionNode memory initialAssertion, bytes32 assertionHash) internal {

236:     function confirmAssertionInternal(
237:         bytes32 assertionHash,
238:         bytes32 parentAssertionHash,
239:         AssertionState calldata confirmState,
240:         bytes32 inboxAcc
241:     ) internal {

274:     function createNewStake(address stakerAddress, uint256 depositAmount, address withdrawalAddress) internal {

286:     function increaseStakeBy(address stakerAddress, uint256 amountAdded) internal {

300:     function reduceStakeTo(address stakerAddress, uint256 target) internal returns (uint256) {

317:     function withdrawStaker(address stakerAddress) internal {

331:     function withdrawFunds(address account) internal returns (uint256) {

343:     function increaseWithdrawableFunds(address account, uint256 amount) internal {

355:     function deleteStaker(address stakerAddress) private {

365:     function createNewAssertion(
366:         AssertionInputs calldata assertion,
367:         bytes32 prevAssertionHash,
368:         bytes32 expectedAssertionHash
369:     ) internal returns (bytes32 newAssertionHash, bool overflowAssertion) {

565:     function requireInactiveStaker(address stakerAddress) internal view {
  • RollupCreator.sol ( 85-87 ):
85:     function createChallengeManager(address rollupAddr, address proxyAdminAddr, Config memory config)
86:         internal
87:         returns (IEdgeChallengeManager)
23:     function assertionHash(
24:         bytes32 parentAssertionHash,
25:         AssertionState memory afterState,
26:         bytes32 inboxAcc
27:     ) internal pure returns (bytes32) {

37:     function assertionHash(
38:         bytes32 parentAssertionHash,
39:         bytes32 afterStateHash,
40:         bytes32 inboxAcc
41:     ) internal pure returns (bytes32) {

54:     function configHash(
55:         bytes32 wasmModuleRoot,
56:         uint256 requiredStake,
57:         address challengeManager,
58:         uint64 confirmPeriodBlocks,
59:         uint64 nextInboxPosition
60:     ) internal pure returns (bytes32) {

73:     function validateConfigHash(
74:         ConfigData calldata configData,
75:         bytes32 _configHash
76:     ) internal pure {
366:     function receiveTokens(uint256 tokenAmount) private {
</details>

[N-04] Names of private/internal state variables should be prefixed with an underscore

It is recommended by the Solidity Style Guide.

<details> <summary>There are 10 instances (click to show):</summary>
91:     uint256 internal constant GAS_PER_BLOB = 1 << 17;

119:     uint64 internal delayBlocks;

120:     uint64 internal futureBlocks;

121:     uint64 internal delaySeconds;

122:     uint64 internal futureSeconds;

129:     uint256 internal immutable deployTimeChainId = block.chainid;

131:     bool internal immutable hostChainIsArbitrum = ArbitrumChecker.runningOnArbitrum();
  • EdgeChallengeManager.sol ( 266-266 ):
266:     EdgeStore internal store;
  • BOLDUpgradeAction.sol ( 99-99 ):
99:     mapping(bytes32 => bytes) internal preImages;
  • RollupUserLogic.sol ( 31-31 ):
31:     uint256 internal immutable deployTimeChainId = block.chainid;
</details>

[N-05] Use of override is unnecessary

Starting from Solidity 0.8.8, the override keyword is not required when overriding an interface function, except for the case where the function is defined in multiple bases.

<details> <summary>There are 33 instances (click to show):</summary>
560:     function addSequencerL2Batch(
561:         uint256 sequenceNumber,
562:         bytes calldata data,
563:         uint256 afterDelayedMessagesRead,
564:         IGasRefunder gasRefunder,
565:         uint256 prevMessageCount,
566:         uint256 newMessageCount
567:     ) external override refundsGas(gasRefunder, IReader4844(address(0))) {
18:     function initialize(Config calldata config, ContractDependencies calldata connectedContracts)
19:         external
20:         override
21:         onlyProxy
22:         initializer
23:     {

117:     function setOutbox(IOutbox _outbox) external override {

127:     function removeOldOutbox(address _outbox) external override {

138:     function setDelayedInbox(address _inbox, bool _enabled) external override {

150:     function pause() external override {

158:     function resume() external override {

166:     function _authorizeUpgrade(address newImplementation) internal override {}

171:     function _authorizeSecondaryUpgrade(address newImplementation) internal override {}

180:     function setValidator(address[] calldata _validator, bool[] calldata _val) external override {

195:     function setOwner(address newOwner) external override {

204:     function setMinimumAssertionPeriod(uint256 newPeriod) external override {

213:     function setConfirmPeriodBlocks(uint64 newConfirmPeriod) external override {

223:     function setBaseStake(uint256 newBaseStake) external override {

228:     function forceRefundStaker(address[] calldata staker) external override whenPaused {

237:     function forceCreateAssertion(
238:         bytes32 prevAssertionHash,
239:         AssertionInputs calldata assertion,
240:         bytes32 expectedAssertionHash
241:     ) external override whenPaused {

258:     function forceConfirmAssertion(
259:         bytes32 assertionHash,
260:         bytes32 parentAssertionHash,
261:         AssertionState calldata confirmState,
262:         bytes32 inboxAcc
263:     ) external override whenPaused {

269:     function setLoserStakeEscrow(address newLoserStakerEscrow) external override {

281:     function setWasmModuleRoot(bytes32 newWasmModuleRoot) external override {

290:     function setSequencerInbox(address _sequencerInbox) external override {
134:     function getAssertion(bytes32 assertionHash) public view override returns (AssertionNode memory) {

145:     function getAssertionCreationBlockForLogLookup(bytes32 assertionHash) external view override returns (uint256) {

162:     function getStakerAddress(uint64 stakerNum) external view override returns (address) {

171:     function isStaked(address staker) public view override returns (bool) {

180:     function latestStakedAssertion(address staker) public view override returns (bytes32) {

189:     function amountStaked(address staker) public view override returns (uint256) {

198:     function getStaker(address staker) external view override returns (Staker memory) {

207:     function withdrawableFunds(address user) external view override returns (uint256) {

212:     function latestConfirmed() public view override returns (bytes32) {

217:     function stakerCount() public view override returns (uint64) {
27:     function initialize(address _stakeToken) external view override onlyProxy {

222:     function returnOldDeposit() external override onlyValidator whenNotPaused {

358:     function withdrawStakerFunds() external override whenNotPaused returns (uint256) {
</details>

[N-06] Add inline comments for unnamed parameters

function func(address a, address) -> function func(address a, address /* b */)

There are 5 instances:

95:     function isBatchPoster(address) external view returns (bool);

97:     function isSequencer(address) external view returns (bool);

124:     function dasKeySetInfo(bytes32) external view returns (bool, uint64);
356:     function addSequencerL2BatchFromOrigin(
357:         uint256,
358:         bytes calldata,
359:         uint256,
360:         IGasRefunder
361:     ) external pure {
  • IAssertionChain.sol ( 26-26 ):
26:     function isValidator(address) external view returns (bool);

[N-07] Consider providing a ranged getter for array state variables

While the compiler automatically provides a getter for accessing single elements within a public state variable array, it doesn't provide a way to fetch the whole array, or subsets thereof. Consider adding a function to allow the fetching of slices of the array, especially if the contract doesn't already have multicall functionality.

There is 1 instance:

  • EdgeChallengeManager.sol ( 279-279 ):
279:     uint256[] public stakeAmounts;

[N-08] Consider splitting complex checks into multiple steps

Assign the expression's parts to intermediate local variables, and check against those instead.

There is 1 instance:

  • RollupProxy.sol ( 16-18 ):
16:             _getAdmin() == address(0) &&
17:             _getImplementation() == address(0) &&
18:             _getSecondaryImplementation() == address(0)

[N-09] Cast to bytes for clearer semantic meaning

Using a cast on a single argument, rather than abi.encodePacked() makes the intended operation more clear, leading to less reviewer confusion.

There is 1 instance:

  • EdgeChallengeManagerLib.sol ( 135-135 ):
135:     bytes32 public constant UNRIVALED = keccak256(abi.encodePacked("UNRIVALED"));

[N-10] Missing checks for empty bytes when updating bytes state variables

Unless the code is attempting to delete the state variable, the caller shouldn't have to write "" to the state variable

There are 4 instances:

  • AssertionStakingPool.sol ( 36-36 ):
36:         assertionHash = _assertionHash;
  • EdgeStakingPool.sol ( 40-40 ):
40:         edgeId = _edgeId;
  • RollupAdminLogic.sol ( 282-282 ):
282:         wasmModuleRoot = newWasmModuleRoot;
229:         _latestConfirmed = assertionHash;

[N-11] Consider adding a block/deny-list

Doing so will significantly increase centralization, but will help to prevent hackers from using stolen tokens

There are 4 instances:

  • AbsBoldStakingPool.sol ( 16 ):
16: abstract contract AbsBoldStakingPool is IAbsBoldStakingPool {
  • EdgeChallengeManager.sol ( 206 ):
206: contract EdgeChallengeManager is IEdgeChallengeManager, Initializable {
  • RollupCreator.sol ( 17 ):
17: contract RollupCreator is Ownable {
  • RollupUserLogic.sol ( 15 ):
15: contract RollupUserLogic is RollupCore, UUPSNotUpgradeable, IRollupUser {

[N-12] Constants/Immutables redefined elsewhere

Consider defining in only one contract so that values cannot become out of sync when only one location is updated. A cheap way to store constants/immutables in a single location is to create an internal constant in a library. If the variable is a local cache of another contract's value, consider making the cache variable internal or private, which will require external users to query the contract with the source of truth, so that callers don't get out of sync.

There are 4 instances:

  • AssertionStakingPool.sol ( 25-25 ):
/// @audit Seen in src/rollup/BOLDUpgradeAction.sol#124
25:     address public immutable rollup;
/// @audit Seen in src/rollup/RollupUserLogic.sol#31
129:     uint256 internal immutable deployTimeChainId = block.chainid;
  • BOLDUpgradeAction.sol ( 124-124 ):
/// @audit Seen in src/assertionStakingPool/AssertionStakingPool.sol#25
124:     IOldRollup public immutable rollup;
  • RollupUserLogic.sol ( 31-31 ):
/// @audit Seen in src/bridge/SequencerInbox.sol#129
31:     uint256 internal immutable deployTimeChainId = block.chainid;

[N-13] Convert simple if-statements to ternary expressions

Converting some if statements to ternaries (such as z = (a < b) ? x : y) can make the code more concise and easier to read.

There are 2 instances:

129:             } else if (val != 0) {
130:                 // accum represents the smaller sub trees, since it is earlier in the expansion
131:                 // we put the larger subtrees on the left
132:                 accum = keccak256(abi.encodePacked(val, accum));
133:             } else {
134:                 // by definition we always complete trees by appending zeros to the right
135:                 accum = keccak256(abi.encodePacked(accum, bytes32(0)));
136:             }
451:             if (afterInboxPosition == currentInboxPosition) {
452:                 // No new messages have been added to the inbox since the last assertion
453:                 // In this case if we set the next inbox position to the current one we would be insisting that
454:                 // the next assertion process no messages. So instead we increment the next inbox position to current
455:                 // plus one, so that the next assertion will process exactly one message.
456:                 // Thus, no assertion can be empty (except the genesis assertion, which is created
457:                 // via a different codepath).
458:                 nextInboxPosition = currentInboxPosition + 1;
459:             } else {
460:                 nextInboxPosition = currentInboxPosition;
461:             }

[N-14] Contracts should each be defined in separate files

Keeping each contract in a separate file makes it easier to work with multiple people, makes the code easier to maintain, and is a common practice on most projects. The following files each contains more than one contract/library/interface.

There are 2 instances:

[N-15] Contract name does not match its filename

According to the Solidity Style Guide, contract and library names should also match their filenames.

There are 3 instances:

  • Assertion.sol ( 66 ):
/// @audit Not match filename `Assertion.sol`
66: library AssertionNodeLib {
  • AssertionState.sol ( 17 ):
/// @audit Not match filename `AssertionState.sol`
17: library AssertionStateLib {
  • IRollupLogic.sol ( 12 ):
/// @audit Not match filename `IRollupLogic.sol`
12: interface IRollupUser is IRollupCore, IOwnable {

[N-16] UPPER_CASE names should be reserved for constant/immutable variables

If the variable needs to be different based on which class it comes from, a view/pure function should be used instead (e.g. like this).

There are 5 instances:

  • SequencerInbox.sol ( 99 ):
99:     ISequencerInbox.MaxTimeVariation private __LEGACY_MAX_TIME_VARIATION;
291:     uint256 public LAYERZERO_BLOCKEDGE_HEIGHT;

293:     uint256 public LAYERZERO_BIGSTEPEDGE_HEIGHT;

295:     uint256 public LAYERZERO_SMALLSTEPEDGE_HEIGHT;

298:     uint8 public NUM_BIGSTEP_LEVEL;

[N-17] Consider emitting an event at the end of the constructor

This will allow users to easily exactly pinpoint when and by whom a contract was constructed.

<details> <summary>There are 10 instances (click to show):</summary>
  • AbsBoldStakingPool.sol ( 24-24 ):
24:     constructor(address _stakeToken) {
  • AssertionStakingPool.sol ( 31-34 ):
31:     constructor(
32:         address _rollup,
33:         bytes32 _assertionHash
34:     ) AbsBoldStakingPool(IRollupCore(_rollup).stakeToken()) {
  • EdgeStakingPool.sol ( 35-38 ):
35:     constructor(
36:         address _challengeManager,
37:         bytes32 _edgeId
38:     ) AbsBoldStakingPool(address(EdgeChallengeManager(_challengeManager).stakeToken())) {
140:     constructor(
141:         uint256 _maxDataSize,
142:         IReader4844 reader4844_,
143:         bool _isUsingFeeToken,
144:         bool _isDelayBufferable
145:     ) {
  • EdgeChallengeManager.sol ( 300-300 ):
300:     constructor() {
126:     constructor(IOldRollup _rollup) {

169:     constructor(uint256[] memory __array) {

287:     constructor(
288:         Contracts memory contracts,
289:         ProxyAdmins memory proxyAdmins,
290:         Implementations memory implementations,
291:         Settings memory settings
292:     ) {
  • BridgeCreator.sol ( 45-48 ):
45:     constructor(
46:         BridgeTemplates memory _ethBasedTemplates,
47:         BridgeTemplates memory _erc20BasedTemplates
48:     ) Ownable() {
  • RollupCreator.sol ( 58-58 ):
58:     constructor() Ownable() {}
</details>

[N-18] Empty function body without comments

Empty function body in solidity is not recommended, consider adding some comments to the body.

There are 3 instances:

166:     function _authorizeUpgrade(address newImplementation) internal override {}

171:     function _authorizeSecondaryUpgrade(address newImplementation) internal override {}
  • RollupCreator.sol ( 61-61 ):
61:     receive() external payable {}

[N-19] Events are emitted without the sender information

When an action is triggered based on a user's action, not being able to filter based on who triggered the action makes event processing a lot more cumbersome. Including the msg.sender the events of these types of action will make events much more useful to end users, especially when msg.sender is not tx.origin.

<details> <summary>There are 34 instances (click to show):</summary>
  • AssertionStakingPoolCreator.sol ( 20-20 ):
20:         emit NewAssertionPoolCreated(_rollup, _assertionHash, address(assertionPool));
  • EdgeStakingPoolCreator.sol ( 20-20 ):
20:         emit NewEdgeStakingPoolCreated(challengeManager, edgeId);
344:         emit SequencerBatchDelivered(
345:             seqMessageIndex,
346:             beforeAcc,
347:             afterAcc,
348:             delayedAcc,
349:             totalDelayedMessagesRead,
350:             timeBounds,
351:             IBridge.BatchDataLocation.NoData
352:         );

488:         emit SequencerBatchDelivered(
489:             sequenceNumber,
490:             beforeAcc,
491:             afterAcc,
492:             delayedAcc,
493:             totalDelayedMessagesRead,
494:             timeBounds,
495:             IBridge.BatchDataLocation.Blob
496:         );
437:         emit EdgeAdded(
438:             edgeAdded.edgeId,
439:             edgeAdded.mutualId,
440:             edgeAdded.originId,
441:             edgeAdded.claimId,
442:             edgeAdded.length,
443:             edgeAdded.level,
444:             edgeAdded.hasRival,
445:             edgeAdded.isLayerZero
446:         );

462:             emit EdgeAdded(
463:                 lowerChildAdded.edgeId,
464:                 lowerChildAdded.mutualId,
465:                 lowerChildAdded.originId,
466:                 lowerChildAdded.claimId,
467:                 lowerChildAdded.length,
468:                 lowerChildAdded.level,
469:                 lowerChildAdded.hasRival,
470:                 lowerChildAdded.isLayerZero
471:             );

474:         emit EdgeAdded(
475:             upperChildAdded.edgeId,
476:             upperChildAdded.mutualId,
477:             upperChildAdded.originId,
478:             upperChildAdded.claimId,
479:             upperChildAdded.length,
480:             upperChildAdded.level,
481:             upperChildAdded.hasRival,
482:             upperChildAdded.isLayerZero
483:         );

485:         emit EdgeBisected(edgeId, lowerChildId, upperChildAdded.edgeId, lowerChildAlreadyExists);

494:             if (updated) emit TimerCacheUpdated(edgeIds[i], newValue);

501:         if (updated) emit TimerCacheUpdated(edgeId, newValue);

507:         if (updated) emit TimerCacheUpdated(edgeId, newValue);

535:         emit EdgeConfirmedByTime(edgeId, store.edges[edgeId].mutualId(), totalTimeUnrivaled);

568:         emit EdgeConfirmedByOneStepProof(edgeId, store.edges[edgeId].mutualId());

584:         emit EdgeRefunded(edgeId, store.edges[edgeId].mutualId(), address(st), sa);
  • BOLDUpgradeAction.sol ( 109-109 ):
109:         emit HashSet(h, executionState, inboxMaxCount);
120:         emit OwnerFunctionCalled(0);

130:         emit OwnerFunctionCalled(1);

140:         emit OwnerFunctionCalled(2);

152:         emit OwnerFunctionCalled(3);

160:         emit OwnerFunctionCalled(4);

187:         emit OwnerFunctionCalled(6);

206:         emit OwnerFunctionCalled(8);

216:         emit OwnerFunctionCalled(9);

225:         emit OwnerFunctionCalled(12);

234:         emit OwnerFunctionCalled(22);

255:         emit OwnerFunctionCalled(23);

266:         emit OwnerFunctionCalled(24);

274:         emit OwnerFunctionCalled(25);

283:         emit OwnerFunctionCalled(26);

292:         emit OwnerFunctionCalled(27);

301:         emit OwnerFunctionCalled(28);

310:         emit OwnerFunctionCalled(30);

319:         emit OwnerFunctionCalled(31);

328:         emit OwnerFunctionCalled(32);
</details>

[N-20] Inconsistent floating version pragma

Source files are using different floating version syntax, this is prone to compilation errors, and is not conducive to the code reliability and maintainability.

There are 4 instances:

  • AbsBoldStakingPool.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • DelayBufferTypes.sol ( 7 ):
7: pragma solidity >=0.6.9 <0.9.0;
  • EdgeChallengeManager.sol ( 5 ):
5: pragma solidity ^0.8.17;
  • Error.sol ( 5 ):
5: pragma solidity ^0.8.4;

[N-21] Function state mutability can be restricted to pure

Function state mutability can be restricted to pure

There are 2 instances:

166:     function _authorizeUpgrade(address newImplementation) internal override {}

171:     function _authorizeSecondaryUpgrade(address newImplementation) internal override {}

[N-22] Imports could be organized more systematically

The contract's interface should be imported first, followed by each of the interfaces it uses, followed by all other files. The examples below do not follow this layout.

<details> <summary>There are 28 instances (click to show):</summary>
  • AbsBoldStakingPool.sol ( 9-9 ):
/// @audit Out of order with the prev import️ ⬆
9: import {IAbsBoldStakingPool} from "./interfaces/IAbsBoldStakingPool.sol";
  • AssertionStakingPool.sol ( 11-11 ):
/// @audit Out of order with the prev import️ ⬆
11: import "./interfaces/IAssertionStakingPool.sol";
  • AssertionStakingPoolCreator.sol ( 10-10 ):
/// @audit Out of order with the prev import️ ⬆
10: import "./interfaces/IAssertionStakingPoolCreator.sol";
/// @audit Out of order with the prev import️ ⬆
8: import "./interfaces/IEdgeStakingPool.sol";

/// @audit Out of order with the prev import️ ⬆
11: import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
  • EdgeStakingPoolCreator.sol ( 10-10 ):
/// @audit Out of order with the prev import️ ⬆
10: import "./interfaces/IEdgeStakingPoolCreator.sol";
  • IEdgeStakingPool.sol ( 8-8 ):
/// @audit Out of order with the prev import️ ⬆
8: import "./IAbsBoldStakingPool.sol";
/// @audit Out of order with the prev import️ ⬆
40: import "./IBridge.sol";

/// @audit Out of order with the prev import️ ⬆
47: import "../libraries/IReader4844.sol";

/// @audit Out of order with the prev import️ ⬆
51: import {IGasRefunder} from "../libraries/IGasRefunder.sol";

/// @audit Out of order with the prev import️ ⬆
54: import {IERC20Bridge} from "./IERC20Bridge.sol";
  • EdgeChallengeManager.sol ( 9-9 ):
/// @audit Out of order with the prev import️ ⬆
9: import "./IAssertionChain.sol";
  • EdgeChallengeManagerLib.sol ( 10-10 ):
/// @audit Out of order with the prev import️ ⬆
10: import "../../osp/IOneStepProofEntry.sol";
  • AssertionState.sol ( 9-9 ):
/// @audit Out of order with the prev import️ ⬆
9: import "../osp/IOneStepProofEntry.sol";
  • BridgeCreator.sol ( 17-17 ):
/// @audit Out of order with the prev import️ ⬆
17: import "../bridge/IBridge.sol";
  • Config.sol ( 9-9 ):
/// @audit Out of order with the prev import️ ⬆
9: import "../bridge/ISequencerInbox.sol";
/// @audit Out of order with the prev import️ ⬆
8: import "../bridge/IBridge.sol";

/// @audit Out of order with the prev import️ ⬆
13: import "../challengeV2/IAssertionChain.sol";
  • RollupAdminLogic.sol ( 10-10 ):
/// @audit Out of order with the prev import️ ⬆
10: import "../bridge/IOutbox.sol";
/// @audit Out of order with the prev import️ ⬆
11: import "./IRollupEventInbox.sol";

/// @audit Out of order with the prev import️ ⬆
16: import "../bridge/ISequencerInbox.sol";
/// @audit Out of order with the prev import️ ⬆
8: import "./IRollupAdmin.sol";

/// @audit Out of order with the prev import️ ⬆
10: import "@offchainlabs/upgrade-executor/src/IUpgradeExecutor.sol";

/// @audit Out of order with the prev import️ ⬆
15: import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/// @audit Out of order with the prev import️ ⬆
8: import "../bridge/ISequencerInbox.sol";

/// @audit Out of order with the prev import️ ⬆
14: import "./IRollupEventInbox.sol";
  • RollupProxy.sol ( 8-8 ):
/// @audit Out of order with the prev import️ ⬆
8: import "./IRollupAdmin.sol";
  • RollupUserLogic.sol ( 12-12 ):
/// @audit Out of order with the prev import️ ⬆
12: import "./IRollupLogic.sol";
</details>

[N-23] There is no need to initialize bool variables with false

Since the bool variables are automatically set to false when created, it is redundant to initialize it with false again.

There is 1 instance:

187:         bool actualIsUsingFeeToken = false;

[N-24] @openzeppelin/contracts should be upgraded to a newer version

These contracts import contracts from @openzeppelin/contracts, but they are not using the latest version. Using the latest version ensures contract security with fixes for known vulnerabilities, access to the latest feature updates, and enhanced community support and engagement.

The imported version is 4.7.3.

<details> <summary>There are 20 instances (click to show):</summary>
  • AbsBoldStakingPool.sol ( 7, 8 ):
7: import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

8: import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
  • AssertionStakingPool.sol ( 8 ):
8: import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
  • EdgeStakingPool.sol ( 11, 12 ):
11: import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

12: import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
  • StakingPoolCreatorUtils.sol ( 8, 9 ):
8: import "@openzeppelin/contracts/utils/Create2.sol";

9: import "@openzeppelin/contracts/utils/Address.sol";
  • EdgeChallengeManager.sol ( 14, 15 ):
14: import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

15: import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
  • BOLDUpgradeAction.sol ( 7, 8 ):
7: import "@openzeppelin/contracts-upgradeable/utils/Create2Upgradeable.sol";

8: import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
  • BridgeCreator.sol ( 18, 19 ):
18: import "@openzeppelin/contracts/access/Ownable.sol";

19: import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
  • RollupAdminLogic.sol ( 13 ):
13: import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
  • RollupCore.sol ( 7 ):
7: import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
11: import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";

12: import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

13: import "@openzeppelin/contracts/access/Ownable.sol";

15: import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
  • RollupUserLogic.sol ( 7 ):
7: import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
</details>

[N-25] Lib @openzeppelin/contracts-upgradeable should be upgraded to a newer version

These contracts import contracts from @openzeppelin/contracts-upgradeable, but they are not using the latest version. Using the latest version ensures contract security with fixes for known vulnerabilities, access to the latest feature updates, and enhanced community support and engagement.

The imported version is 4.7.3.

There are 3 instances:

  • EdgeChallengeManager.sol ( 14 ):
14: import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
  • BOLDUpgradeAction.sol ( 7 ):
7: import "@openzeppelin/contracts-upgradeable/utils/Create2Upgradeable.sol";
  • RollupCore.sol ( 7 ):
7: import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";

[N-26] Expressions for constant values should use immutable rather than constant

While it doesn't save any gas because the compiler knows that developers often make this mistake, it's still best to use the right tool for the task at hand. There is a difference between constant variables and immutable variables, and they should each be used in their appropriate contexts. constants should be used for literal values written into the code, and immutable variables should be used for expressions, or values calculated in, or passed into the constructor.

There is 1 instance:

  • EdgeChallengeManagerLib.sol ( 135-135 ):
135:     bytes32 public constant UNRIVALED = keccak256(abi.encodePacked("UNRIVALED"));

[N-27] Consider moving duplicated strings to constants

Moving duplicate strings to constants can improve code maintainability and readability.

<details> <summary>There are 22 instances (click to show):</summary>
  • ArrayUtilsLib.sol ( 32-32 ):
32:         require(startIndex < endIndex, "Start not less than end");
112:         require(me.length <= MAX_LEVEL, "Merkle expansion too large");

160:         require(me.length <= MAX_LEVEL, "Merkle expansion too large");

265:         require(startSize < endSize, "Start not less than end");
15:         require(x > 0, "Zero has no significant bits");

30:         require(x != 0, "Zero has no significant bits");
57:         require(config.loserStakeEscrow != address(0), "INVALID_ESCROW_0");

181:         require(_validator.length > 0, "EMPTY_ARRAY");

229:         require(staker.length > 0, "EMPTY_ARRAY");

272:         require(newLoserStakerEscrow != address(0), "INVALID_ESCROW_0");
357:         require(staker.isStaked, "NOT_STAKED");

566:         require(isStaked(stakerAddress), "NOT_STAKED");
154:                 "SI_MAX_DATA_SIZE_MISMATCH"

158:                 "SI_MAX_DATA_SIZE_MISMATCH"

160:             require(deployParams.maxDataSize == ethInbox.maxDataSize(), "I_MAX_DATA_SIZE_MISMATCH");

172:                 "SI_MAX_DATA_SIZE_MISMATCH"

176:                 "SI_MAX_DATA_SIZE_MISMATCH"

180:                 "I_MAX_DATA_SIZE_MISMATCH"
63:         require(!validatorWhitelistDisabled, "WHITELIST_DISABLED");

72:         require(!validatorWhitelistDisabled, "WHITELIST_DISABLED");

175:         require(isStaked(msg.sender), "NOT_STAKED");

233:         require(isStaked(stakerAddress), "NOT_STAKED");
</details>

[N-28] Contracts containing only utility functions should be made into libraries

Consider using a library instead of a contract to provide utility functions.

There are 3 instances:

  • AssertionStakingPoolCreator.sol ( 13 ):
13: contract AssertionStakingPoolCreator is IAssertionStakingPoolCreator {
  • EdgeStakingPoolCreator.sol ( 13 ):
13: contract EdgeStakingPoolCreator is IEdgeStakingPoolCreator {
  • RollupProxy.sol ( 11 ):
11: contract RollupProxy is AdminFallbackProxy {

[N-29] Functions should be named in mixedCase style

As the Solidity Style Guide suggests: functions should be named in mixedCase style.

There are 7 instances:

55:     function HEADER_LENGTH() external view returns (uint256);

61:     function DATA_AUTHENTICATED_FLAG() external view returns (bytes1);

67:     function DATA_BLOB_HEADER_FLAG() external view returns (bytes1);

73:     function DAS_MESSAGE_HEADER_FLAG() external view returns (bytes1);

79:     function TREE_DAS_MESSAGE_HEADER_FLAG() external view returns (bytes1);

85:     function BROTLI_MESSAGE_HEADER_FLAG() external view returns (bytes1);

91:     function ZERO_HEAVY_MESSAGE_HEADER_FLAG() external view returns (bytes1);

[N-30] Variable names for immutables should use UPPER_CASE_WITH_UNDERSCORES

For immutable variable names, each word should use all capital letters, with underscores separating each word (UPPER_CASE_WITH_UNDERSCORES)

<details> <summary>There are 14 instances (click to show):</summary>
  • AbsBoldStakingPool.sol ( 20-20 ):
20:     address public immutable stakeToken;
25:     address public immutable rollup;

27:     bytes32 public immutable assertionHash;
29:     address public immutable challengeManager;

31:     bytes32 public immutable edgeId;
116:     IReader4844 public immutable reader4844;

128:     uint256 public immutable maxDataSize;

129:     uint256 internal immutable deployTimeChainId = block.chainid;

131:     bool internal immutable hostChainIsArbitrum = ArbitrumChecker.runningOnArbitrum();

133:     bool public immutable isUsingFeeToken;

135:     bool public immutable isDelayBufferable;
  • BOLDUpgradeAction.sol ( 124-124 ):
124:     IOldRollup public immutable rollup;
112:     bool internal immutable _hostChainIsArbitrum = ArbitrumChecker.runningOnArbitrum();
  • RollupUserLogic.sol ( 31-31 ):
31:     uint256 internal immutable deployTimeChainId = block.chainid;
</details>

[N-31] Names of public/external functions and state variables should NOT be prefixed with underscore

It is recommended by the Solidity Style Guide.

There is 1 instance:

102:     mapping(address => Staker) public _stakerMap;

[N-32] Overridden function has no body

Consider adding a NatSpec comment describing why the function doesn't need a body and or the purpose it serves.

There are 2 instances:

166:     function _authorizeUpgrade(address newImplementation) internal override {}

171:     function _authorizeSecondaryUpgrade(address newImplementation) internal override {}

[N-33] Named imports of parent contracts are missing

Consider adding named imports for all parent contracts.

<details> <summary>There are 25 instances (click to show):</summary>
  • AssertionStakingPool.sol ( 21-21 ):
/// @audit AbsBoldStakingPool
/// @audit IAssertionStakingPool
21: contract AssertionStakingPool is AbsBoldStakingPool, IAssertionStakingPool {
  • AssertionStakingPoolCreator.sol ( 13-13 ):
/// @audit IAssertionStakingPoolCreator
13: contract AssertionStakingPoolCreator is IAssertionStakingPoolCreator {
  • EdgeStakingPool.sol ( 25-25 ):
/// @audit AbsBoldStakingPool
/// @audit IEdgeStakingPool
25: contract EdgeStakingPool is AbsBoldStakingPool, IEdgeStakingPool {
  • EdgeStakingPoolCreator.sol ( 13-13 ):
/// @audit IEdgeStakingPoolCreator
13: contract EdgeStakingPoolCreator is IEdgeStakingPoolCreator {
  • IAssertionStakingPool.sol ( 10-10 ):
/// @audit IAbsBoldStakingPool
10: interface IAssertionStakingPool is IAbsBoldStakingPool {
  • IEdgeStakingPool.sol ( 10-10 ):
/// @audit IAbsBoldStakingPool
10: interface IEdgeStakingPool is IAbsBoldStakingPool {
  • ISequencerInbox.sol ( 15-15 ):
/// @audit IDelayedMessageProvider
15: interface ISequencerInbox is IDelayedMessageProvider {
  • SequencerInbox.sol ( 64-64 ):
/// @audit DelegateCallAware
/// @audit ISequencerInbox
64: contract SequencerInbox is DelegateCallAware, GasRefundEnabled, ISequencerInbox {
  • EdgeChallengeManager.sol ( 206-206 ):
/// @audit Initializable
206: contract EdgeChallengeManager is IEdgeChallengeManager, Initializable {
  • BridgeCreator.sol ( 21-21 ):
/// @audit Ownable
21: contract BridgeCreator is Ownable {
  • IRollupCore.sol ( 15-15 ):
/// @audit IAssertionChain
15: interface IRollupCore is IAssertionChain {
  • IRollupLogic.sol ( 12-12 ):
/// @audit IRollupCore
/// @audit IOwnable
12: interface IRollupUser is IRollupCore, IOwnable {
  • RollupAdminLogic.sol ( 15-15 ):
/// @audit RollupCore
/// @audit IRollupAdmin
/// @audit DoubleLogicUUPSUpgradeable
15: contract RollupAdminLogic is RollupCore, IRollupAdmin, DoubleLogicUUPSUpgradeable {
  • RollupCore.sol ( 22-22 ):
/// @audit IRollupCore
/// @audit PausableUpgradeable
22: abstract contract RollupCore is IRollupCore, PausableUpgradeable {
  • RollupCreator.sol ( 17-17 ):
/// @audit Ownable
17: contract RollupCreator is Ownable {
  • RollupProxy.sol ( 11-11 ):
/// @audit AdminFallbackProxy
11: contract RollupProxy is AdminFallbackProxy {
  • RollupUserLogic.sol ( 15-15 ):
/// @audit RollupCore
/// @audit UUPSNotUpgradeable
15: contract RollupUserLogic is RollupCore, UUPSNotUpgradeable, IRollupUser {
</details>

[N-34] Constants should be put on the left side of comparisons

Putting constants on the left side of comparison statements is a best practice known as Yoda conditions. Although solidity's static typing system prevents accidental assignments within conditionals, adopting this practice can improve code readability and consistency, especially when working across multiple languages.

<details> <summary>There are 106 instances (click to show):</summary>
  • AbsBoldStakingPool.sol ( 30, 42 ):
/// @audit put `0` on the left
30:         if (amount == 0) {

/// @audit put `0` on the left
42:         if (amount == 0) {
/// @audit put `0` on the left
117:             config.threshold != 0 &&

/// @audit put `0` on the left
118:             config.max != 0 &&

/// @audit put `BASIS` on the left
119:             config.replenishRateInBasis <= BASIS &&
/// @audit put `IReader4844(address(0))` on the left
148:             if (reader4844_ != IReader4844(address(0))) revert DataBlobsNotSupported();

/// @audit put `IReader4844(address(0))` on the left
150:             if (reader4844_ == IReader4844(address(0))) revert InitParamZero("Reader4844");

/// @audit put `0` on the left
170:         if (buffer.bufferBlocks != 0) {

/// @audit put `IBridge(address(0))` on the left
182:         if (bridge != IBridge(address(0))) revert AlreadyInit();

/// @audit put `IBridge(address(0))` on the left
183:         if (bridge_ == IBridge(address(0))) revert HadZeroInit();

/// @audit put `address(0)` on the left
189:             if (feeToken != address(0)) {

/// @audit put `~uint256(0)` on the left
484:         if (seqMessageIndex != sequenceNumber && sequenceNumber != ~uint256(0)) {

/// @audit put `~uint256(0)` on the left
538:         if (seqMessageIndex != sequenceNumber && sequenceNumber != ~uint256(0)) {

/// @audit put `HEADER_LENGTH` on the left
651:         assert(header.length == HEADER_LENGTH);

/// @audit put `BROTLI_MESSAGE_HEADER_FLAG` on the left
677:             headerByte == BROTLI_MESSAGE_HEADER_FLAG ||

/// @audit put `DAS_MESSAGE_HEADER_FLAG` on the left
678:             headerByte == DAS_MESSAGE_HEADER_FLAG ||

/// @audit put `ZERO_HEAVY_MESSAGE_HEADER_FLAG` on the left
680:             headerByte == ZERO_HEAVY_MESSAGE_HEADER_FLAG;

/// @audit put `33` on the left
711:             if (data[0] & DAS_MESSAGE_HEADER_FLAG != 0 && data.length >= 33) {

/// @audit put `0` on the left
736:         if (dataHashes.length == 0) revert MissingDataHashes();

/// @audit put `0` on the left
851:         if (buffer.bufferBlocks == 0 || buffer.bufferBlocks > bufferConfig_.max) {

/// @audit put `64 * 1024` on the left
906:         if (keysetBytes.length >= 64 * 1024) revert KeysetTooLarge();

/// @audit put `0` on the left
959:         if (ksInfo.creationBlock == 0) revert NoSuchKeyset(ksHash);
/// @audit put `0` on the left
325:         if (_challengePeriodBlocks == 0) {

/// @audit put `address(0)` on the left
331:         if (_excessStakeReceiver == address(0)) {

/// @audit put `0` on the left
350:         if (_numBigStepLevel == 0) {

/// @audit put `0` on the left
387:             if (args.proof.length == 0) {

/// @audit put `0` on the left
429:         if (address(st) != address(0) && sa != 0) {

/// @audit put `0` on the left
458:         bool lowerChildAlreadyExists = lowerChildAdded.edgeId == 0;

/// @audit put `0` on the left
580:         if (address(st) != address(0) && sa != 0) {
/// @audit put `0` on the left
76:         if (originId == 0) {

/// @audit put `0` on the left
82:         if (startHistoryRoot == 0) {

/// @audit put `0` on the left
85:         if (endHistoryRoot == 0) {

/// @audit put `address(0)` on the left
103:         if (staker == address(0)) {

/// @audit put `0` on the left
106:         if (claimId == 0) {

/// @audit put `0` on the left
224:         return edge.createdAtBlock != 0;

/// @audit put `0` on the left
231:         if (len == 0) {

/// @audit put `0` on the left
240:         if (edge.lowerChildId != 0 || edge.upperChildId != 0) {

/// @audit put `0` on the left
240:         if (edge.lowerChildId != 0 || edge.upperChildId != 0) {

/// @audit put `0` on the left
259:         return edge.claimId != 0 && edge.staker != address(0);

/// @audit put `address(0)` on the left
259:         return edge.claimId != 0 && edge.staker != address(0);

/// @audit put `true` on the left
271:         if (edge.refunded == true) {

/// @audit put `0` on the left
280:         if (level == 0) {
/// @audit put `0` on the left
182:         if (firstRival == 0) {

/// @audit put `UNRIVALED` on the left
184:         } else if (firstRival == UNRIVALED) {

/// @audit put `0` on the left
198:             firstRival != 0,

/// @audit put `0` on the left
199:             edge.claimId != 0

/// @audit put `0` on the left
229:             if (ard.assertionHash == 0) {

/// @audit put `0` on the left
248:             if (args.proof.length == 0) {

/// @audit put `0` on the left
294:             if (args.proof.length == 0) {

/// @audit put `0` on the left
329:         if (x == 0) {

/// @audit put `0` on the left
378:         if (args.prefixProof.length == 0) {

/// @audit put `0` on the left
472:         if (firstRival == 0) {

/// @audit put `UNRIVALED` on the left
477:         return firstRival != UNRIVALED;

/// @audit put `bytes32(0)` on the left
490:         if (store.edges[edgeId].lowerChildId != bytes32(0)) {

/// @audit put `0` on the left
545:         if (firstRival == 0) {

/// @audit put `UNRIVALED` on the left
551:         if (firstRival == UNRIVALED) {

/// @audit put `bytes32(0)` on the left
665:         if (confirmedRivalId != bytes32(0)) {
/// @audit put `MAX_LEVEL` on the left
112:         require(me.length <= MAX_LEVEL, "Merkle expansion too large");

/// @audit put `0` on the left
117:             if (accum == 0) {

/// @audit put `0` on the left
118:                 if (val != 0) {

/// @audit put `0` on the left
129:             } else if (val != 0) {

/// @audit put `0` on the left
159:         require(subtreeRoot != 0, "Cannot append empty subtree");

/// @audit put `MAX_LEVEL` on the left
160:         require(me.length <= MAX_LEVEL, "Merkle expansion too large");

/// @audit put `0` on the left
162:         if (me.length == 0) {

/// @audit put `MAX_LEVEL` on the left
183:         require(next.length <= MAX_LEVEL, "Append creates oversize tree");

/// @audit put `0` on the left
195:                 require(me[i] == 0, "Append above least significant bit");

/// @audit put `0` on the left
198:                 if (accumHash == 0) {

/// @audit put `0` on the left
203:                     if (me[i] == 0) {

/// @audit put `0` on the left
222:         if (accumHash != 0) {

/// @audit put `0` on the left
228:         require(next[next.length - 1] != 0, "Last entry zero");

/// @audit put `0` on the left
275:         if (y != 0) {

/// @audit put `0` on the left
282:         if (z != 0) {

/// @audit put `0` on the left
295:             if (me[i] != 0) {
/// @audit put `0` on the left
30:         require(x != 0, "Zero has no significant bits");

/// @audit put `0x100000000000000000000000000000000` on the left
33:         if (x >= 0x100000000000000000000000000000000) {

/// @audit put `0x10000000000000000` on the left
38:         if (x >= 0x10000000000000000) {

/// @audit put `0x100000000` on the left
43:         if (x >= 0x100000000) {

/// @audit put `0x10000` on the left
48:         if (x >= 0x10000) {

/// @audit put `0x100` on the left
53:         if (x >= 0x100) {

/// @audit put `0x10` on the left
58:         if (x >= 0x10) {

/// @audit put `0x4` on the left
63:         if (x >= 0x4) {

/// @audit put `0x2` on the left
68:         if (x >= 0x2) msb += 1;
  • Assertion.sol ( 86, 88 ):
/// @audit put `0` on the left
86:         if (self.firstChildBlock == 0) {

/// @audit put `0` on the left
88:         } else if (self.secondChildBlock == 0) {
/// @audit put `0` on the left
114:         require(inboxMaxCount != 0, "Hash not yet set");

/// @audit put `0` on the left
353:             if (staker.isStaked && staker.currentChallenge == 0) {

/// @audit put `0` on the left
526:         if (validators.length != 0) {
/// @audit put `0` on the left
107:         bool isDelayBufferable = bufferConfig.threshold != 0;

/// @audit put `address(0)` on the left
112:             nativeToken == address(0) ? ethBasedTemplates : erc20BasedTemplates,

/// @audit put `address(0)` on the left
117:         if (nativeToken == address(0)) {
  • RollupAdminLogic.sol ( 57, 272 ):
/// @audit put `address(0)` on the left
57:         require(config.loserStakeEscrow != address(0), "INVALID_ESCROW_0");

/// @audit put `address(0)` on the left
272:         require(newLoserStakerEscrow != address(0), "INVALID_ESCROW_0");
/// @audit put `bytes32(0)` on the left
127:         require(assertionHash != bytes32(0), "ASSERTION_ID_CANNOT_BE_ZERO");

/// @audit put `0` on the left
427:             require(afterStateCmpMaxInbox <= 0, "INBOX_TOO_FAR");

/// @audit put `0` on the left
466:             require(afterInboxPosition != 0, "EMPTY_INBOX_COUNT");

/// @audit put `bytes32(0)` on the left
478:             newAssertionHash == expectedAssertionHash || expectedAssertionHash == bytes32(0),

/// @audit put `0` on the left
489:             prevAssertion.firstChildBlock == 0, // assumes block 0 is impossible
/// @audit put `address(0)` on the left
228:         if (deployParams.batchPosterManager != address(0)) {

/// @audit put `0` on the left
233:         if (deployParams.validators.length != 0) {

/// @audit put `address(0)` on the left
292:         if (_nativeToken == address(0)) {
/// @audit put `address(0)` on the left
28:         require(_stakeToken != address(0), "NEED_STAKE_TOKEN");

/// @audit put `0` on the left
53:         if (latestConfirmedAssertion.createdAtBlock == 0) return false;

/// @audit put `0` on the left
120:             require(winningEdge.confirmedAtBlock != 0, "ZERO_CONFIRMED_AT_BLOCK");

/// @audit put `bytes32(0)` on the left
170:             expectedAssertionHash == bytes32(0)

/// @audit put `bytes32(0)` on the left
278:         require(expectedAssertionHash != bytes32(0), "EXPECTED_ASSERTION_HASH");

/// @audit put `address(0)` on the left
337:         require(withdrawalAddress != address(0), "EMPTY_WITHDRAWAL_ADDRESS");
</details>

[N-35] else-block not required

One level of nesting can be removed by not having an else block when the if-block always jumps at the end. For example:

if (condition) {
    body1...
    return x;
} else {
    body2...
}

can be changed to:

if (condition) {
    body1...
    return x;
}
body2...
<details> <summary>There are 12 instances (click to show):</summary>
  • StakingPoolCreatorUtils.sol ( 16-18 ):
16:         if (Address.isContract(pool)) {
17:             return pool;
18:         } else {
279:         if (_chainIdChanged()) {
280:             return (1, 1, 1, 1);
281:         } else {
592:         if (eType == EdgeType.Block) {
593:             return LAYERZERO_BLOCKEDGE_HEIGHT;
594:         } else if (eType == EdgeType.BigStep) {

594:         } else if (eType == EdgeType.BigStep) {
595:             return LAYERZERO_BIGSTEPEDGE_HEIGHT;
596:         } else if (eType == EdgeType.SmallStep) {

596:         } else if (eType == EdgeType.SmallStep) {
597:             return LAYERZERO_SMALLSTEPEDGE_HEIGHT;
598:         } else {
280:         if (level == 0) {
281:             return EdgeType.Block;
282:         } else if (level <= numBigStepLevels) {

282:         } else if (level <= numBigStepLevels) {
283:             return EdgeType.BigStep;
284:         } else if (level == numBigStepLevels + 1) {

284:         } else if (level == numBigStepLevels + 1) {
285:             return EdgeType.SmallStep;
286:         } else {
220:         if (ChallengeEdgeLib.levelToType(args.level, numBigStepLevel) == EdgeType.Block) {
221:             // origin id is the assertion which is the root of challenge
222:             // all rivals and their children share the same origin id - it is a link to the information
223:             // they agree on
224:             bytes32 originId = ard.predecessorId;
225: 
226:             // Sanity check: The assertion reference data should be related to the claim
227:             // Of course the caller can provide whatever args they wish, so this is really just a helpful
228:             // check to avoid mistakes
229:             if (ard.assertionHash == 0) {
230:                 revert AssertionHashEmpty();
231:             }
232:             if (ard.assertionHash != args.claimId) {
233:                 revert AssertionHashMismatch(ard.assertionHash, args.claimId);
234:             }
235: 
236:             // if the assertion is already confirmed or rejected then it cant be referenced as a claim
237:             if (!ard.isPending) {
238:                 revert AssertionNotPending();
239:             }
240: 
241:             // if the claim doesnt have a sibling then it is undisputed, there's no need
242:             // to open challenge edges for it
243:             if (!ard.hasSibling) {
244:                 revert AssertionNoSibling();
245:             }
246: 
247:             // parse the inclusion proof for later use
248:             if (args.proof.length == 0) {
249:                 revert EmptyEdgeSpecificProof();
250:             }
251:             (bytes32[] memory inclusionProof,,) =
252:                 abi.decode(args.proof, (bytes32[], AssertionStateData, AssertionStateData));
253: 
254:             // check the start and end execution states exist, the block hash entry should be non zero
255:             if (ard.startState.machineStatus == MachineStatus.RUNNING) {
256:                 revert EmptyStartMachineStatus();
257:             }
258:             if (ard.endState.machineStatus == MachineStatus.RUNNING) {
259:                 revert EmptyEndMachineStatus();
260:             }
261: 
262:             // Create machine hashes out of the proven state
263:             bytes32 startStateHash = oneStepProofEntry.getMachineHash(ard.startState.toExecutionState());
264:             bytes32 endStateHash = oneStepProofEntry.getMachineHash(ard.endState.toExecutionState());
265: 
266:             return (ProofData(startStateHash, endStateHash, inclusionProof), originId);
267:         } else {

551:         if (firstRival == UNRIVALED) {
552:             return block.number - store.edges[edgeId].createdAtBlock;
553:         } else {

562:             if (firstRivalCreatedAtBlock > edgeCreatedAtBlock) {
563:                 // if this edge was created before the first rival then we return the difference
564:                 // in createdAtBlock number
565:                 return firstRivalCreatedAtBlock - edgeCreatedAtBlock;
566:             } else {
146:         if (_hostChainIsArbitrum) {
147:             uint256 blockNum = _assertionCreatedAtArbSysBlock[assertionHash];
148:             require(blockNum > 0, "NO_ASSERTION");
149:             return blockNum;
150:         } else {
</details>

[N-36] Large multiples of ten should use scientific notation

Use a scientific notation rather than decimal literals (e.g. 1e6 instead of 1000000), for better code readability.

There is 1 instance:

  • DelayBuffer.sol ( 17-17 ):
/// @audit 10000 -> 1e4
17:     uint256 public constant BASIS = 10000;

[N-37] TODOs left in the code

TODOs may signal that a feature is missing or not ready for audit, consider resolving the issue and removing the TODO comment

There is 1 instance:

  • ChallengeEdgeLib.sol ( 63 ):
63:     /// @notice TODO

[N-38] High cyclomatic complexity

Consider breaking down these blocks into more manageable units, by splitting things into utility functions, by reducing nesting, and by using early returns.

<details> <summary>There is 1 instance (click to show):</summary>
  • EdgeChallengeManager.sol ( 305-364 ):
305:     function initialize(
306:         IAssertionChain _assertionChain,
307:         uint64 _challengePeriodBlocks,
308:         IOneStepProofEntry _oneStepProofEntry,
309:         uint256 layerZeroBlockEdgeHeight,
310:         uint256 layerZeroBigStepEdgeHeight,
311:         uint256 layerZeroSmallStepEdgeHeight,
312:         IERC20 _stakeToken,
313:         address _excessStakeReceiver,
314:         uint8 _numBigStepLevel,
315:         uint256[] calldata _stakeAmounts
316:     ) public initializer {
317:         if (address(_assertionChain) == address(0)) {
318:             revert EmptyAssertionChain();
319:         }
320:         assertionChain = _assertionChain;
321:         if (address(_oneStepProofEntry) == address(0)) {
322:             revert EmptyOneStepProofEntry();
323:         }
324:         oneStepProofEntry = _oneStepProofEntry;
325:         if (_challengePeriodBlocks == 0) {
326:             revert EmptyChallengePeriod();
327:         }
328:         challengePeriodBlocks = _challengePeriodBlocks;
329: 
...... OMITTED ......
340:         if (!EdgeChallengeManagerLib.isPowerOfTwo(layerZeroBigStepEdgeHeight)) {
341:             revert NotPowerOfTwo(layerZeroBigStepEdgeHeight);
342:         }
343:         LAYERZERO_BIGSTEPEDGE_HEIGHT = layerZeroBigStepEdgeHeight;
344:         if (!EdgeChallengeManagerLib.isPowerOfTwo(layerZeroSmallStepEdgeHeight)) {
345:             revert NotPowerOfTwo(layerZeroSmallStepEdgeHeight);
346:         }
347:         LAYERZERO_SMALLSTEPEDGE_HEIGHT = layerZeroSmallStepEdgeHeight;
348: 
349:         // ensure that there is at least on of each type of level
350:         if (_numBigStepLevel == 0) {
351:             revert ZeroBigStepLevels();
352:         }
353:         // ensure there's also space for the block level and the small step level
354:         // in total level parameters
355:         if (_numBigStepLevel > 253) {
356:             revert BigStepLevelsTooMany(_numBigStepLevel);
357:         }
358:         NUM_BIGSTEP_LEVEL = _numBigStepLevel;
359: 
360:         if (_numBigStepLevel + 2 != _stakeAmounts.length) {
361:             revert StakeAmountsMismatch(_stakeAmounts.length, _numBigStepLevel + 2);
362:         }
363:         stakeAmounts = _stakeAmounts;
364:     }
</details>

[N-39] Typos

All typos should be corrected.

<details> <summary>There are 18 instances (click to show):</summary>
  • SequencerInbox.sol ( 608 ):
/// @audit proccessed
608:         // buffer update depends on new delayed messages. if none are read, no buffer update is proccessed
  • ArrayUtilsLib.sol ( 23, 26 ):
/// @audit exlusive
23:     /// @dev    End index exlusive so slice(arr, 0, 5) gets the first 5 elements of arr

/// @audit exlusive
26:     /// @param endIndex     The end index of the slice in the original array - exlusive
  • EdgeChallengeManagerLib.sol ( 274, 793 ):
/// @audit existance
274:             // hasLengthOneRival checks existance, so we can use getNoCheck

/// @audit preceeding
793:         // To do this we sum the machine steps of the edges in each of the preceeding levels.
/// @audit leve
156:         // we use number representations of the levels elsewhere, so we need to ensure we're appending a leve

/// @audit numbe
176:         // if by appending the sub tree we increase the numbe of most sig bits of the size, that means

/// @audit storeage
221:         // increased the storeage above
  • UintUtilsLib.sol ( 13, 28 ):
/// @audit signficant
13:     /// @param x Cannot be zero, since zero that has no signficant bits

/// @audit sigificant
28:     /// @param x Cannot be zero, since zero has no sigificant bits
  • Error.sol ( 35 ):
/// @audit adddress
35: /// @param addr The adddress in question
  • BOLDUpgradeAction.sol ( 517 ):
/// @audit UNEXPCTED
517:         require(address(rollup) == expectedRollupAddress, "UNEXPCTED_ROLLUP_ADDR");
/// @audit lastest
570:         bytes32 lastestAssertion = latestStakedAssertion(stakerAddress);

/// @audit lastest
571:         bool isLatestConfirmed = lastestAssertion == latestConfirmed();

/// @audit lastest
572:         bool haveChild = getAssertionStorage(lastestAssertion).firstChildBlock > 0;
  • RollupLib.sol ( 53 ):
/// @audit emited
53:     // All these should be emited in AssertionCreated event
  • RollupUserLogic.sol ( 220, 238 ):
/// @audit chlid
220:      * @notice Refund a staker that is currently staked on an assertion that either has a chlid assertion or is the latest confirmed assertion.

/// @audit creditted
238:      * @notice Reduce the amount staked for the sender (difference between initial amount staked and target is creditted back to the sender).
</details>

[N-40] Consider bounding input array length

The functions below take in an unbounded array, and make function calls for entries in the array. While the function will revert if it eventually runs out of gas, it may be a nicer user experience to require() that the length of the array is below some reasonable maximum, so that the user doesn't have to use up a full transaction's gas only to see that the transaction reverts.

There are 4 instances:

  • EdgeChallengeManager.sol ( 492 ):
492:         for (uint256 i = 0; i < edgeIds.length; i++) {
  • BOLDUpgradeAction.sol ( 528 ):
528:             for (uint256 i = 0; i < validators.length; i++) {
  • RollupAdminLogic.sol ( 184, 230 ):
184:         for (uint256 i = 0; i < _validator.length; i++) {

230:         for (uint256 i = 0; i < staker.length; i++) {

[N-41] Unnecessary casting

Unnecessary castings can be removed.

<details> <summary>There are 11 instances (click to show):</summary>
  • EdgeStakingPool.sol ( 46-46 ):
/// @audit address(challengeManager)
46:         IERC20(stakeToken).safeIncreaseAllowance(address(challengeManager), requiredStake);
/// @audit IOwnable(rollup)
209:         if (msg.sender != IOwnable(rollup).owner())

/// @audit IOwnable(rollup)
210:             revert NotOwner(msg.sender, IOwnable(rollup).owner());
  • BOLDUpgradeAction.sol ( 475-475 ):
/// @audit address(IMPL_CHALLENGE_MANAGER)
475:                     address(IMPL_CHALLENGE_MANAGER),
/// @audit IBridge(frame.bridge)
122:         frame.sequencerInbox.initialize(IBridge(frame.bridge), maxTimeVariation, bufferConfig);
  • RollupAdminLogic.sol ( 139-139 ):
/// @audit address(_inbox)
139:         bridge.setDelayedInbox(address(_inbox), _enabled);
/// @audit address(upgradeExecutor)
204:         proxyAdmin.transferOwnership(address(upgradeExecutor));

/// @audit address(upgradeExecutor)
241:         IRollupAdmin(address(rollup)).setOwner(address(upgradeExecutor));

/// @audit address(upgradeExecutor)
261:             address(upgradeExecutor),

/// @audit address(validatorWalletCreator)
262:             address(validatorWalletCreator)
  • RollupProxy.sol ( 21-21 ):
/// @audit address(connectedContracts.rollupAdminLogic)
21:                 address(connectedContracts.rollupAdminLogic),
</details>

[N-42] Unused contract variables

The following state variables are defined but not used. It is recommended to check the code for logical omissions that cause them not to be used. If it's determined that they are not needed anywhere, it's best to remove them from the codebase to improve code clarity and minimize confusion.

There is 1 instance:

  • SequencerInbox.sol ( 99-99 ):
99:     ISequencerInbox.MaxTimeVariation private __LEGACY_MAX_TIME_VARIATION;

[N-43] Unused function parameters

Function parameters that are defined but not used may be logical errors and need to be checked to see if any logic is missing. If the parameter is not really needed, it should be removed, otherwise it will repeatedly cause confusion and make code maintenance difficult. If the parameter cannot be removed directly (for example, if it is an override function), its name should be removed and some comment can be appended.

There are 2 instances:

/// @audit newImplementation
166:     function _authorizeUpgrade(address newImplementation) internal override {}

/// @audit newImplementation
171:     function _authorizeSecondaryUpgrade(address newImplementation) internal override {}

[N-44] Unused named return

Declaring named returns, but not using them, is confusing to the reader. Consider either completely removing them (by declaring just the type without a name), or remove the return statement and do a variable assignment. This would improve the readability of the code, and it may also help reduce regressions during future code refactors.

There are 2 instances:

  • ChallengeEdgeLib.sol ( 279-279 ):
/// @audit eType
279:     function levelToType(uint8 level, uint8 numBigStepLevels) internal pure returns (EdgeType eType) {
  • UintUtilsLib.sol ( 14-14 ):
/// @audit msb
14:     function leastSignificantBit(uint256 x) internal pure returns (uint256 msb) {

[N-45] Use delete instead of assigning values to false

The delete keyword more closely matches the semantics of what is being done, and draws more attention to the changing of state, which may lead to a more thorough audit of its associated logic.

There is 1 instance:

927:         dasKeySetInfo[ksHash].isValidKeyset = false;

[N-46] Consider using delete rather than assigning zero to clear values

The delete keyword more closely matches the semantics of what is being done, and draws more attention to the changing of state, which may lead to a more thorough audit of its associated logic.

There are 3 instances:

207:                         accumHash = 0;

212:                         next[i] = 0;
333:         _withdrawableFunds[account] = 0;

[N-47] Use the latest Solidity version

Upgrading to the latest solidity version (0.8.19 for L2s) can optimize gas usage, take advantage of new features and improve overall contract efficiency. Where possible, based on compatibility requirements, it is recommended to use newer/latest solidity version to take advantage of the latest optimizations and features.

<details> <summary>There are 24 instances (click to show):</summary>
  • AbsBoldStakingPool.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • AssertionStakingPool.sol ( 6 ):
6: pragma solidity ^0.8.0;
  • AssertionStakingPoolCreator.sol ( 6 ):
6: pragma solidity ^0.8.0;
  • EdgeStakingPool.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • EdgeStakingPoolCreator.sol ( 6 ):
6: pragma solidity ^0.8.0;
  • StakingPoolCreatorUtils.sol ( 6 ):
6: pragma solidity ^0.8.0;
  • DelayBuffer.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • SequencerInbox.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • EdgeChallengeManager.sol ( 5 ):
5: pragma solidity ^0.8.17;
  • ArrayUtilsLib.sol ( 5 ):
5: pragma solidity ^0.8.17;
  • ChallengeEdgeLib.sol ( 5 ):
5: pragma solidity ^0.8.17;
  • EdgeChallengeManagerLib.sol ( 5 ):
5: pragma solidity ^0.8.17;
  • MerkleTreeLib.sol ( 5 ):
5: pragma solidity ^0.8.17;
  • UintUtilsLib.sol ( 5 ):
5: pragma solidity ^0.8.17;
  • Assertion.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • AssertionState.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • BOLDUpgradeAction.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • BridgeCreator.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • RollupAdminLogic.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • RollupCore.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • RollupCreator.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • RollupLib.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • RollupProxy.sol ( 5 ):
5: pragma solidity ^0.8.0;
  • RollupUserLogic.sol ( 5 ):
5: pragma solidity ^0.8.0;
</details>

[N-48] Use transfer libraries instead of low level calls to transfer native tokens

Consider using SafeTransferLib.safeTransferETH or Address.sendValue for clearer semantic meaning instead of using a low level call.

There is 1 instance:

304:             (bool sent, ) = msg.sender.call{value: address(this).balance}("");

[N-49] Using underscore at the end of variable name

The use of the underscore at the end of the variable name is unusual, consider refactoring it.

<details> <summary>There are 27 instances (click to show):</summary>
/// @audit maxTimeVariation_
247:     function setMaxTimeVariation(MaxTimeVariation memory maxTimeVariation_) external;

/// @audit isBatchPoster_
254:     function setIsBatchPoster(address addr, bool isBatchPoster_) external;

/// @audit isSequencer_
274:     function setIsSequencer(address addr, bool isSequencer_) external;

/// @audit bridge_
288:         IBridge bridge_,

/// @audit maxTimeVariation_
289:         MaxTimeVariation calldata maxTimeVariation_,

/// @audit bufferConfig_
290:         BufferConfig calldata bufferConfig_
/// @audit reader4844_
142:         IReader4844 reader4844_,

/// @audit bufferConfig_
161:     function postUpgradeInit(BufferConfig memory bufferConfig_)

/// @audit bridge_
178:         IBridge bridge_,

/// @audit maxTimeVariation_
179:         ISequencerInbox.MaxTimeVariation calldata maxTimeVariation_,

/// @audit bufferConfig_
180:         BufferConfig memory bufferConfig_

/// @audit delayBlocks_
219:             uint64 delayBlocks_,

/// @audit futureBlocks_
220:             uint64 futureBlocks_,

/// @audit delaySeconds_
221:             uint64 delaySeconds_,

/// @audit futureSeconds_
222:             uint64 futureSeconds_

/// @audit delayBlocks_
255:             uint64 delayBlocks_,

/// @audit futureBlocks_
256:             uint64 futureBlocks_,

/// @audit delaySeconds_
257:             uint64 delaySeconds_,

/// @audit futureSeconds_
258:             uint64 futureSeconds_

/// @audit delayBlocks_
306:         uint256 delayBlocks_ = delayBlocks;

/// @audit bufferConfig_
847:     function _setBufferConfig(BufferConfig memory bufferConfig_) internal {

/// @audit maxTimeVariation_
867:     function _setMaxTimeVariation(ISequencerInbox.MaxTimeVariation memory maxTimeVariation_)

/// @audit maxTimeVariation_
885:     function setMaxTimeVariation(ISequencerInbox.MaxTimeVariation memory maxTimeVariation_)

/// @audit isBatchPoster_
894:     function setIsBatchPoster(address addr, bool isBatchPoster_)

/// @audit isSequencer_
933:     function setIsSequencer(address addr, bool isSequencer_)

/// @audit bufferConfig_
947:     function setBufferConfig(BufferConfig memory bufferConfig_) external onlyRollupOwner {
  • BOLDUpgradeAction.sol ( 84-84 ):
/// @audit bufferConfig_
84:     function postUpgradeInit(BufferConfig memory bufferConfig_) external;
</details>

[N-50] Use a struct to encapsulate multiple function parameters

If a function has too many parameters, replacing them with a struct can improve code readability and maintainability, increase reusability, and reduce the likelihood of errors when passing the parameters.

<details> <summary>There are 9 instances (click to show):</summary>
139:     function forceInclusion(
140:         uint256 _totalDelayedMessagesRead,
141:         uint8 kind,
142:         uint64[2] calldata l1BlockAndTime,
143:         uint256 baseFeeL1,
144:         address sender,
145:         bytes32 messageDataHash
146:     ) external;

179:     function addSequencerL2BatchFromOrigin(
180:         uint256 sequenceNumber,
181:         bytes calldata data,
182:         uint256 afterDelayedMessagesRead,
183:         IGasRefunder gasRefunder,
184:         uint256 prevMessageCount,
185:         uint256 newMessageCount
186:     ) external;

188:     function addSequencerL2Batch(
189:         uint256 sequenceNumber,
190:         bytes calldata data,
191:         uint256 afterDelayedMessagesRead,
192:         IGasRefunder gasRefunder,
193:         uint256 prevMessageCount,
194:         uint256 newMessageCount
195:     ) external;

197:     function addSequencerL2BatchFromBlobs(
198:         uint256 sequenceNumber,
199:         uint256 afterDelayedMessagesRead,
200:         IGasRefunder gasRefunder,
201:         uint256 prevMessageCount,
202:         uint256 newMessageCount
203:     ) external;

207:     function addSequencerL2BatchFromBlobsDelayProof(
208:         uint256 sequenceNumber,
209:         uint256 afterDelayedMessagesRead,
210:         IGasRefunder gasRefunder,
211:         uint256 prevMessageCount,
212:         uint256 newMessageCount,
213:         DelayProof calldata delayProof
214:     ) external;

219:     function addSequencerL2BatchFromOriginDelayProof(
220:         uint256 sequenceNumber,
221:         bytes calldata data,
222:         uint256 afterDelayedMessagesRead,
223:         IGasRefunder gasRefunder,
224:         uint256 prevMessageCount,
225:         uint256 newMessageCount,
226:         DelayProof calldata delayProof
227:     ) external;

231:     function addSequencerL2BatchDelayProof(
232:         uint256 sequenceNumber,
233:         bytes calldata data,
234:         uint256 afterDelayedMessagesRead,
235:         IGasRefunder gasRefunder,
236:         uint256 prevMessageCount,
237:         uint256 newMessageCount,
238:         DelayProof calldata delayProof
239:     ) external;
287:     function forceInclusion(
288:         uint256 _totalDelayedMessagesRead,
289:         uint8 kind,
290:         uint64[2] calldata l1BlockAndTime,
291:         uint256 baseFeeL1,
292:         address sender,
293:         bytes32 messageDataHash
294:     ) external {

366:     function addSequencerL2BatchFromOrigin(
367:         uint256 sequenceNumber,
368:         bytes calldata data,
369:         uint256 afterDelayedMessagesRead,
370:         IGasRefunder gasRefunder,
371:         uint256 prevMessageCount,
372:         uint256 newMessageCount
373:     ) external refundsGas(gasRefunder, IReader4844(address(0))) {
</details>

[N-51] Returning a struct instead of a bunch of variables is better

If a function returns too many variables, replacing them with a struct can improve code readability, maintainability and reusability.

<details> <summary>There are 7 instances (click to show):</summary>
114:     function maxTimeVariation()
115:         external
116:         view
117:         returns (
118:             uint256 delayBlocks,
119:             uint256 futureBlocks,
120:             uint256 delaySeconds,
121:             uint256 futureSeconds
122:         );

124:     function dasKeySetInfo(bytes32) external view returns (bool, uint64);
244:     function maxTimeVariation()
245:         external
246:         view
247:         returns (
248:             uint256,
249:             uint256,
250:             uint256,
251:             uint256
252:         )

269:     function maxTimeVariationInternal()
270:         internal
271:         view
272:         returns (
273:             uint64,
274:             uint64,
275:             uint64,
276:             uint64
277:         )

637:     function packHeader(uint256 afterDelayedMessagesRead)
638:         internal
639:         view
640:         returns (bytes memory, IBridge.TimeBounds memory)

659:     function formEmptyDataHash(uint256 afterDelayedMessagesRead)
660:         internal
661:         view
662:         returns (bytes32, IBridge.TimeBounds memory)

688:     function formCallDataHash(bytes calldata data, uint256 afterDelayedMessagesRead)
689:         internal
690:         view
691:         returns (bytes32, IBridge.TimeBounds memory)
</details>

[N-52] Contract variables should have comments

Consider adding some comments on non-public contract variables to explain what they are supposed to do. This will help for future code reviews.

<details> <summary>There are 11 instances (click to show):</summary>
120:     uint64 internal futureBlocks;

121:     uint64 internal delaySeconds;

122:     uint64 internal futureSeconds;

129:     uint256 internal immutable deployTimeChainId = block.chainid;
99:     mapping(bytes32 => bytes) internal preImages;

167:     uint256[] _array;
98:     bytes32 private _latestConfirmed;

99:     mapping(bytes32 => AssertionNode) private _assertions;

101:     address[] private _stakerList;

104:     mapping(address => uint256) private _withdrawableFunds;
  • RollupUserLogic.sol ( 31-31 ):
31:     uint256 internal immutable deployTimeChainId = block.chainid;
</details>

[N-53] Missing event when a state variables is set in constructor

The initial states set in a constructor are important and should be recorded in the event.

There are 3 instances:

  • BOLDUpgradeAction.sol ( 170 ):
170:         _array = __array;
  • BridgeCreator.sol ( 49, 50 ):
49:         ethBasedTemplates = _ethBasedTemplates;

50:         erc20BasedTemplates = _erc20BasedTemplates;

[N-54] Empty bytes check is missing

Passing empty bytes to a function can cause unexpected behavior, such as certain operations failing, producing incorrect results, or wasting gas. It is recommended to check that all byte parameters are not empty.

<details> <summary>There are 5 instances (click to show):</summary>
/// @audit data
366:     function addSequencerL2BatchFromOrigin(
367:         uint256 sequenceNumber,
368:         bytes calldata data,
369:         uint256 afterDelayedMessagesRead,
370:         IGasRefunder gasRefunder,
371:         uint256 prevMessageCount,
372:         uint256 newMessageCount
373:     ) external refundsGas(gasRefunder, IReader4844(address(0))) {

/// @audit data
430:     function addSequencerL2BatchFromOriginDelayProof(
431:         uint256 sequenceNumber,
432:         bytes calldata data,
433:         uint256 afterDelayedMessagesRead,
434:         IGasRefunder gasRefunder,
435:         uint256 prevMessageCount,
436:         uint256 newMessageCount,
437:         DelayProof calldata delayProof
438:     ) external refundsGas(gasRefunder, IReader4844(address(0))) {

/// @audit data
560:     function addSequencerL2Batch(
561:         uint256 sequenceNumber,
562:         bytes calldata data,
563:         uint256 afterDelayedMessagesRead,
564:         IGasRefunder gasRefunder,
565:         uint256 prevMessageCount,
566:         uint256 newMessageCount
567:     ) external override refundsGas(gasRefunder, IReader4844(address(0))) {

/// @audit data
582:     function addSequencerL2BatchDelayProof(
583:         uint256 sequenceNumber,
584:         bytes calldata data,
585:         uint256 afterDelayedMessagesRead,
586:         IGasRefunder gasRefunder,
587:         uint256 prevMessageCount,
588:         uint256 newMessageCount,
589:         DelayProof calldata delayProof
590:     ) external refundsGas(gasRefunder, IReader4844(address(0))) {
  • EdgeChallengeManager.sol ( 451-454 ):
/// @audit prefixProof
451:     function bisectEdge(bytes32 edgeId, bytes32 bisectionHistoryRoot, bytes calldata prefixProof)
452:         external
453:         returns (bytes32, bytes32)
454:     {
</details>

[N-55] Don't define functions with the same name in a contract

In Solidity, while function overriding allows for functions with the same name to coexist, it is advisable to avoid this practice to enhance code readability and maintainability. Having multiple functions with the same name, even with different parameters or in inherited contracts, can cause confusion and increase the likelihood of errors during development, testing, and debugging. Using distinct and descriptive function names not only clarifies the purpose and behavior of each function, but also helps prevent unintended function calls or incorrect overriding. By adopting a clear and consistent naming convention, developers can create more comprehensible and maintainable smart contracts.

<details> <summary>There are 7 instances (click to show):</summary>
  • AbsBoldStakingPool.sol ( 57 ):
/// @audit Different function with same name found on line 41
57:     function withdrawFromPool() external {
  • IAbsBoldStakingPool.sol ( 27 ):
/// @audit Different function with same name found on line 24
27:     function withdrawFromPool() external;
  • ISequencerInbox.sol ( 179 ):
/// @audit Different function with same name found on line 171
179:     function addSequencerL2BatchFromOrigin(
  • SequencerInbox.sol ( 366 ):
/// @audit Different function with same name found on line 356
366:     function addSequencerL2BatchFromOrigin(
  • IRollupLogic.sol ( 44 ):
/// @audit Different function with same name found on line 38
44:     function newStakeOnNewAssertion(
  • RollupLib.sol ( 37 ):
/// @audit Different function with same name found on line 23
37:     function assertionHash(
  • RollupUserLogic.sol ( 331 ):
/// @audit Different function with same name found on line 316
331:     function newStakeOnNewAssertion(
</details>

[N-56] Multiple mappings with same keys can be combined into a single struct mapping for readability

Well-organized data structures make code reviews easier, which may lead to fewer bugs. Consider combining related mappings into mappings to structs, so it's clear what data is related.

There are 7 instances:

95:     mapping(address => bool) public isBatchPoster;

115:     mapping(address => bool) public isSequencer;
96:     mapping(address => bool) public isValidator;

99:     mapping(bytes32 => AssertionNode) private _assertions;

102:     mapping(address => Staker) public _stakerMap;

104:     mapping(address => uint256) private _withdrawableFunds;

114:     mapping(bytes32 => uint256) internal _assertionCreatedAtArbSysBlock;

[N-57] Do not cache immutable variables

Instead of caching immutable variables, using them directly can make code more consistent and less prone to errors.

There are 5 instances:

415:         TransparentUpgradeableProxy bridge = TransparentUpgradeableProxy(payable(BRIDGE));

421:         TransparentUpgradeableProxy inbox = TransparentUpgradeableProxy(payable(INBOX));

424:         TransparentUpgradeableProxy rollupEventInbox = TransparentUpgradeableProxy(payable(REI));

428:         TransparentUpgradeableProxy outbox = TransparentUpgradeableProxy(payable(OUTBOX));

434:         TransparentUpgradeableProxy sequencerInbox = TransparentUpgradeableProxy(payable(SEQ_INBOX));

[N-58] Missing event for critical changes

Events should be emitted when critical changes are made to the contracts.

<details> <summary>There are 17 instances (click to show):</summary>
  • DelayBuffer.sol ( 68-75 ):
68:     function update(BufferData storage self, uint64 blockNumber) internal {
69:         self.bufferBlocks = calcPendingBuffer(self, blockNumber);
70: 
71:         // store a new starting reference point
72:         // any buffer updates will be applied retroactively in the next batch post
73:         self.prevBlockNumber = blockNumber;
74:         self.prevSequencedBlockNumber = uint64(block.number);
75:     }
208:     function updateRollupAddress() external {
209:         if (msg.sender != IOwnable(rollup).owner())
210:             revert NotOwner(msg.sender, IOwnable(rollup).owner());
211:         IOwnable newRollup = bridge.rollup();
212:         if (rollup == newRollup) revert RollupNotChanged();
213:         rollup = newRollup;
214:     }

236:     function removeDelayAfterFork() external {
237:         if (!_chainIdChanged()) revert NotForked();
238:         delayBlocks = 1;
239:         futureBlocks = 1;
240:         delaySeconds = 1;
241:         futureSeconds = 1;
242:     }

791:     function addSequencerL2BatchImpl(
792:         bytes32 dataHash,
793:         uint256 afterDelayedMessagesRead,
794:         uint256 calldataLengthPosted,
795:         uint256 prevMessageCount,
796:         uint256 newMessageCount
797:     )
798:         internal
799:         returns (
800:             uint256 seqMessageIndex,
801:             bytes32 beforeAcc,
802:             bytes32 delayedAcc,
803:             bytes32 acc
804:         )

847:     function _setBufferConfig(BufferConfig memory bufferConfig_) internal {
848:         if (!isDelayBufferable) revert NotDelayBufferable();
849:         if (!DelayBuffer.isValidBufferConfig(bufferConfig_)) revert BadBufferConfig();
850: 
851:         if (buffer.bufferBlocks == 0 || buffer.bufferBlocks > bufferConfig_.max) {
852:             buffer.bufferBlocks = bufferConfig_.max;
853:         }
854:         if (buffer.bufferBlocks < bufferConfig_.threshold) {
855:             buffer.bufferBlocks = bufferConfig_.threshold;
856:         }
857:         buffer.max = bufferConfig_.max;
858:         buffer.threshold = bufferConfig_.threshold;
859:         buffer.replenishRateInBasis = bufferConfig_.replenishRateInBasis;
860: 
861:         // if all delayed messages are read, the buffer is considered synced
862:         if (bridge.delayedMessageCount() == totalDelayedMessagesRead) {
863:             buffer.update(uint64(block.number));
864:         }
865:     }

867:     function _setMaxTimeVariation(ISequencerInbox.MaxTimeVariation memory maxTimeVariation_)
868:         internal
869:     {
870:         if (
871:             maxTimeVariation_.delayBlocks > type(uint64).max ||
872:             maxTimeVariation_.futureBlocks > type(uint64).max ||
873:             maxTimeVariation_.delaySeconds > type(uint64).max ||
874:             maxTimeVariation_.futureSeconds > type(uint64).max
875:         ) {
876:             revert BadMaxTimeVariation();
877:         }
878:         delayBlocks = uint64(maxTimeVariation_.delayBlocks);
879:         futureBlocks = uint64(maxTimeVariation_.futureBlocks);
880:         delaySeconds = uint64(maxTimeVariation_.delaySeconds);
881:         futureSeconds = uint64(maxTimeVariation_.futureSeconds);
882:     }
239:     function setChildren(ChallengeEdge storage edge, bytes32 lowerChildId, bytes32 upperChildId) internal {
240:         if (edge.lowerChildId != 0 || edge.upperChildId != 0) {
241:             revert ChildrenAlreadySet(ChallengeEdgeLib.id(edge), edge.lowerChildId, edge.upperChildId);
242:         }
243:         edge.lowerChildId = lowerChildId;
244:         edge.upperChildId = upperChildId;
245:     }

249:     function setConfirmed(ChallengeEdge storage edge) internal {
250:         if (edge.status != EdgeStatus.Pending) {
251:             revert EdgeNotPending(ChallengeEdgeLib.id(edge), edge.status);
252:         }
253:         edge.status = EdgeStatus.Confirmed;
254:         edge.confirmedAtBlock = uint64(block.number);
255:     }

264:     function setRefunded(ChallengeEdge storage edge) internal {
265:         if (edge.status != EdgeStatus.Confirmed) {
266:             revert EdgeNotConfirmed(ChallengeEdgeLib.id(edge), edge.status);
267:         }
268:         if (!isLayerZero(edge)) {
269:             revert EdgeNotLayerZero(ChallengeEdgeLib.id(edge), edge.staker, edge.claimId);
270:         }
271:         if (edge.refunded == true) {
272:             revert EdgeAlreadyRefunded(ChallengeEdgeLib.id(edge));
273:         }
274: 
275:         edge.refunded = true;
276:     }
160:     function add(EdgeStore storage store, ChallengeEdge memory edge) internal returns (EdgeAddedData memory) {

502:     function updateTimerCache(EdgeStore storage store, bytes32 edgeId, uint256 newValue)
503:         internal
504:         returns (bool, uint256)
505:     {
506:         uint256 currentAccuTimer = store.edges[edgeId].totalTimeUnrivaledCache;
507:         newValue = newValue > type(uint64).max ? type(uint64).max : newValue;
508:         // only update when increased
509:         if (newValue > currentAccuTimer) {
510:             store.edges[edgeId].totalTimeUnrivaledCache = uint64(newValue);
511:             return (true, newValue);
512:         }
513:         return (false, currentAccuTimer);
514:     }

520:     function updateTimerCacheByClaim(
521:         EdgeStore storage store,
522:         bytes32 edgeId,
523:         bytes32 claimingEdgeId,
524:         uint8 numBigStepLevel
525:     ) internal returns (bool, uint256) {
526:         // calculate the time unrivaled without inheritance
527:         uint256 totalTimeUnrivaled = timeUnrivaled(store, edgeId);
528:         checkClaimIdLink(store, edgeId, claimingEdgeId, numBigStepLevel);
529:         totalTimeUnrivaled += store.edges[claimingEdgeId].totalTimeUnrivaledCache;
530:         return updateTimerCache(store, edgeId, totalTimeUnrivaled);
531:     }

662:     function setConfirmedRival(EdgeStore storage store, bytes32 edgeId) internal {
663:         bytes32 mutualId = store.edges[edgeId].mutualId();
664:         bytes32 confirmedRivalId = store.confirmedRivals[mutualId];
665:         if (confirmedRivalId != bytes32(0)) {
666:             revert RivalEdgeConfirmed(edgeId, confirmedRivalId);
667:         }
668:         store.confirmedRivals[mutualId] = edgeId;
669:     }
355:     function deleteStaker(address stakerAddress) private {
356:         Staker storage staker = _stakerMap[stakerAddress];
357:         require(staker.isStaked, "NOT_STAKED");
358:         uint64 stakerIndex = staker.index;
359:         _stakerList[stakerIndex] = _stakerList[_stakerList.length - 1];
360:         _stakerMap[_stakerList[stakerIndex]].index = stakerIndex;
361:         _stakerList.pop();
362:         delete _stakerMap[stakerAddress];
363:     }
267:     function _deployUpgradeExecutor(address rollupOwner, ProxyAdmin proxyAdmin)
268:         internal
269:         returns (address)
270:     {
271:         IUpgradeExecutor upgradeExecutor = IUpgradeExecutor(
272:             address(
273:                 new TransparentUpgradeableProxy(
274:                     address(upgradeExecutorLogic),
275:                     address(proxyAdmin),
276:                     bytes("")
277:                 )
278:             )
279:         );
280:         address[] memory executors = new address[](1);
281:         executors[0] = rollupOwner;
282:         upgradeExecutor.initialize(address(upgradeExecutor), executors);
283: 
284:         return address(upgradeExecutor);
285:     }
62:     function removeWhitelistAfterFork() external {
63:         require(!validatorWhitelistDisabled, "WHITELIST_DISABLED");
64:         require(_chainIdChanged(), "CHAIN_ID_NOT_CHANGED");
65:         validatorWhitelistDisabled = true;
66:     }

71:     function removeWhitelistAfterValidatorAfk() external {
72:         require(!validatorWhitelistDisabled, "WHITELIST_DISABLED");
73:         require(_validatorIsAfk(), "VALIDATOR_NOT_AFK");
74:         validatorWhitelistDisabled = true;
75:     }
</details>

[N-59] Consider adding emergency-stop functionality

Adding a way to quickly halt protocol functionality in an emergency, rather than having to pause individual contracts one-by-one, will make in-progress hack mitigation faster and much less stressful.

<details> <summary>There are 6 instances (click to show):</summary>
  • EdgeChallengeManager.sol ( 206-206 ):
206: contract EdgeChallengeManager is IEdgeChallengeManager, Initializable {
  • BridgeCreator.sol ( 21-21 ):
21: contract BridgeCreator is Ownable {
  • RollupAdminLogic.sol ( 15-15 ):
15: contract RollupAdminLogic is RollupCore, IRollupAdmin, DoubleLogicUUPSUpgradeable {
  • RollupCreator.sol ( 17-17 ):
17: contract RollupCreator is Ownable {
  • RollupProxy.sol ( 11-11 ):
11: contract RollupProxy is AdminFallbackProxy {
  • RollupUserLogic.sol ( 15-15 ):
15: contract RollupUserLogic is RollupCore, UUPSNotUpgradeable, IRollupUser {
</details>

[N-60] Avoid the use of sensitive terms

Use alternative variants, e.g. allowlist/denylist instead of whitelist/blacklist.

<details> <summary>There are 63 instances (click to show):</summary>
194:     ///         This is only tracked when the validator whitelist is enabled

372:         // Check if whitelist is enabled in the Rollup

373:         // We only enforce whitelist in this function as it may exhaust resources

374:         bool whitelistEnabled = !assertionChain.validatorWhitelistDisabled();

376:         if (whitelistEnabled && !assertionChain.isValidator(msg.sender)) {

418:             args, ard, oneStepProofEntry, expectedEndHeight, NUM_BIGSTEP_LEVEL, whitelistEnabled
  • IAssertionChain.sol ( 27 ):
27:     function validatorWhitelistDisabled() external view returns (bool);
  • ChallengeErrors.sol ( 107 ):
107: /// @dev Thrown when the validator whitelist is enabled and the account attempting to create a layer zero edge is not whitelisted
112:     /// @notice The id of the assertion - will be used in a sanity check

226:             // Sanity check: The assertion reference data should be related to the claim

361:         // However it's a nice sanity check for the calling code to check that their local edge

418:         bool whitelistEnabled

428:         // if the validator whitelist is enabled, we can enforce that a single party cannot create two layer zero edges that rival each other

429:         // if the validator whitelist is disabled, this check serves no purpose since an attacker can create new accounts

430:         if (whitelistEnabled) {

471:         // Sanity check: it should never be possible to create an edge without having an entry in firstRivals

544:         // Sanity check: it's not possible to have a 0 first rival for an edge that exists

554:             // Sanity check: it's not possible an edge does not exist for a first rival record

684:     /// @dev    Does some additional sanity checks to ensure that the claim id link is valid
  • MerkleTreeLib.sol ( 227 ):
227:         // so this is just a sanity check
204:     bool public immutable DISABLE_VALIDATOR_WHITELIST;

245:         bool disableValidatorWhitelist;

327:         DISABLE_VALIDATOR_WHITELIST = settings.disableValidatorWhitelist;

534:         if (DISABLE_VALIDATOR_WHITELIST) {

535:             IRollupAdmin(address(rollup)).setValidatorWhitelistDisabled(DISABLE_VALIDATOR_WHITELIST);
48:      * @notice Set the addresses of the validator whitelist

51:      * @param _validator addresses to set in the whitelist

52:      * @param _val value to set in the whitelist for corresponding address

110:      * @notice set the validatorWhitelistDisabled flag

111:      * @param _validatorWhitelistDisabled new value of validatorWhitelistDisabled, i.e. true = disabled

113:     function setValidatorWhitelistDisabled(bool _validatorWhitelistDisabled) external;
  • IRollupLogic.sol ( 17, 19 ):
17:     function removeWhitelistAfterFork() external;

19:     function removeWhitelistAfterValidatorAfk() external;
174:      * @notice Set the addresses of the validator whitelist

177:      * @param _validator addresses to set in the whitelist

178:      * @param _val value to set in the whitelist for corresponding address

305:      * @notice set the validatorWhitelistDisabled flag

306:      * @param _validatorWhitelistDisabled new value of validatorWhitelistDisabled, i.e. true = disabled

308:     function setValidatorWhitelistDisabled(bool _validatorWhitelistDisabled) external {

309:         validatorWhitelistDisabled = _validatorWhitelistDisabled;
  • RollupCore.sol ( 108, 377 ):
108:     bool public validatorWhitelistDisabled;

377:         // we can do a quick sanity check here
21:         require(isValidator[msg.sender] || validatorWhitelistDisabled, "NOT_VALIDATOR");

38:      * @notice Number of blocks since the last confirmed assertion before the validator whitelist is removed

41:      *         a further 14 days before the validator whitelist is removed.

62:     function removeWhitelistAfterFork() external {

63:         require(!validatorWhitelistDisabled, "WHITELIST_DISABLED");

65:         validatorWhitelistDisabled = true;

69:      * @notice Remove the whitelist after the validator has been inactive for too long

71:     function removeWhitelistAfterValidatorAfk() external {

72:         require(!validatorWhitelistDisabled, "WHITELIST_DISABLED");

74:         validatorWhitelistDisabled = true;
</details>

[N-61] Missing checks for uint state variable assignments

Consider whether reasonable bounds checks for variables would be useful.

There are 6 instances:

328:         challengePeriodBlocks = _challengePeriodBlocks;

339:         LAYERZERO_BLOCKEDGE_HEIGHT = layerZeroBlockEdgeHeight;

343:         LAYERZERO_BIGSTEPEDGE_HEIGHT = layerZeroBigStepEdgeHeight;

347:         LAYERZERO_SMALLSTEPEDGE_HEIGHT = layerZeroSmallStepEdgeHeight;
205:         minimumAssertionPeriod = newPeriod;

224:         baseStake = newBaseStake;

[N-62] Use the Modern Upgradeable Contract Paradigm

Modern smart contract development often employs upgradeable contract structures, utilizing proxy patterns like OpenZeppelin’s Upgradeable Contracts. This paradigm separates logic and state, allowing developers to amend and enhance the contract's functionality without altering its state or the deployed contract address. Transitioning to this approach enhances long-term maintainability. Resolution: Adopt a well-established proxy pattern for upgradeability, ensuring proper initialization and employing transparent proxies to mitigate potential risks. Embrace comprehensive testing and audit practices, particularly when updating contract logic, to ensure state consistency and security are preserved across upgrades. This ensures your contract remains robust and adaptable to future requirements.

<details> <summary>There are 11 instances (click to show):</summary>
  • AssertionStakingPool.sol ( 21 ):
21: contract AssertionStakingPool is AbsBoldStakingPool, IAssertionStakingPool {
  • AssertionStakingPoolCreator.sol ( 13 ):
13: contract AssertionStakingPoolCreator is IAssertionStakingPoolCreator {
  • EdgeStakingPool.sol ( 25 ):
25: contract EdgeStakingPool is AbsBoldStakingPool, IEdgeStakingPool {
  • EdgeStakingPoolCreator.sol ( 13 ):
13: contract EdgeStakingPoolCreator is IEdgeStakingPoolCreator {
  • SequencerInbox.sol ( 64 ):
64: contract SequencerInbox is DelegateCallAware, GasRefundEnabled, ISequencerInbox {
94: contract StateHashPreImageLookup {

123: contract RollupReader is IOldRollup {

166: contract ConstantArrayStorage {

182: contract BOLDUpgradeAction {
  • BridgeCreator.sol ( 21 ):
21: contract BridgeCreator is Ownable {
  • RollupCreator.sol ( 17 ):
17: contract RollupCreator is Ownable {
</details>

[N-63] Large or complicated code bases should implement invariant tests

This includes: large code bases, or code with lots of inline-assembly, complicated math, or complicated interactions between multiple contracts. Invariant fuzzers such as Echidna require the test writer to come up with invariants which should not be violated under any circumstances, and the fuzzer tests various inputs and function calls to ensure that the invariants always hold. Even code with 100% code coverage can still have bugs due to the order of the operations a user performs, and invariant fuzzers may help significantly.

There is 1 instance:

  • Global finding

[N-64] The default value is manually set when it is declared

In instances where a new variable is defined, there is no need to set it to it's default value.

<details> <summary>There are 23 instances (click to show):</summary>
85:     bytes1 public constant BROTLI_MESSAGE_HEADER_FLAG = 0x00;

317:         bytes32 prevDelayedAcc = 0;
492:         for (uint256 i = 0; i < edgeIds.length; i++) {

517:         uint64 assertionBlocks = 0;
15:         for (uint256 i = 0; i < arr.length; i++) {

47:         for (uint256 i = 0; i < arr1.length; i++) {

50:         for (uint256 i = 0; i < arr2.length; i++) {
114:         bytes32 accum = 0;

115:         for (uint256 i = 0; i < me.length; i++) {

188:         for (uint256 i = 0; i < me.length; i++) {

293:         uint256 sum = 0;

294:         for (uint256 i = 0; i < me.length; i++) {

326:         uint256 proofIndex = 0;
350:         for (uint64 i = 0; i < stakerCount; i++) {

528:             for (uint256 i = 0; i < validators.length; i++) {
63:         bytes32 parentAssertionHash = bytes32(0);

64:         bytes32 inboxAcc = bytes32(0);

184:         for (uint256 i = 0; i < _validator.length; i++) {

230:         for (uint256 i = 0; i < staker.length; i++) {
523:         bytes32 parentAssertionHash = bytes32(0);

524:         bytes32 inboxAcc = bytes32(0);
225:         for (uint256 i = 0; i < deployParams.batchPosters.length; i++) {

235:             for (uint256 i = 0; i < deployParams.validators.length; i++) {
</details>

[N-65] Contracts should have all public/external functions exposed by interfaces

All external/public functions should extend an interface. This is useful to ensure that the whole API is extracted and can be more easily integrated by other projects.

<details> <summary>There are 9 instances (click to show):</summary>
  • SequencerInbox.sol ( 64 ):
64: contract SequencerInbox is DelegateCallAware, GasRefundEnabled, ISequencerInbox {
  • BOLDUpgradeAction.sol ( 94, 166, 182 ):
94: contract StateHashPreImageLookup {

166: contract ConstantArrayStorage {

182: contract BOLDUpgradeAction {
  • BridgeCreator.sol ( 21 ):
21: contract BridgeCreator is Ownable {
  • RollupAdminLogic.sol ( 15 ):
15: contract RollupAdminLogic is RollupCore, IRollupAdmin, DoubleLogicUUPSUpgradeable {
  • RollupCreator.sol ( 17 ):
17: contract RollupCreator is Ownable {
  • RollupProxy.sol ( 11 ):
11: contract RollupProxy is AdminFallbackProxy {
  • RollupUserLogic.sol ( 15 ):
15: contract RollupUserLogic is RollupCore, UUPSNotUpgradeable, IRollupUser {
</details>

[N-66] Top-level declarations should be separated by at least two lines

<details> <summary>There are 120 instances (click to show):</summary>
  • AbsBoldStakingPool.sol ( 5-7 ):
5: pragma solidity ^0.8.0;
6: 
7: import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
  • AssertionStakingPool.sol ( 6-8 ):
6: pragma solidity ^0.8.0;
7: 
8: import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
  • AssertionStakingPoolCreator.sol ( 6-8 ):
6: pragma solidity ^0.8.0;
7: 
8: import "./AssertionStakingPool.sol";
  • EdgeStakingPool.sol ( 5-7 ):
5: pragma solidity ^0.8.0;
6: 
7: import "./AbsBoldStakingPool.sol";
  • EdgeStakingPoolCreator.sol ( 6-8 ):
6: pragma solidity ^0.8.0;
7: 
8: import "./EdgeStakingPool.sol";
  • StakingPoolCreatorUtils.sol ( 6-8, 9-11 ):
6: pragma solidity ^0.8.0;
7: 
8: import "@openzeppelin/contracts/utils/Create2.sol";

9: import "@openzeppelin/contracts/utils/Address.sol";
10: 
11: library StakingPoolCreatorUtils {
  • IAbsBoldStakingPool.sol ( 5-7 ):
5: pragma solidity ^0.8.0;
6: 
7: interface IAbsBoldStakingPool {
  • IAssertionStakingPool.sol ( 5-7, 8-10 ):
5: pragma solidity ^0.8.0;
6: 
7: import "../../rollup/IRollupLogic.sol";

8: import "./IAbsBoldStakingPool.sol";
9: 
10: interface IAssertionStakingPool is IAbsBoldStakingPool {
  • IAssertionStakingPoolCreator.sol ( 5-7, 8-10 ):
5: pragma solidity ^0.8.0;
6: 
7: import "../../rollup/IRollupLogic.sol";

8: import "./IAssertionStakingPool.sol";
9: 
10: interface IAssertionStakingPoolCreator {
  • IEdgeStakingPool.sol ( 5-7, 8-10 ):
5: pragma solidity ^0.8.0;
6: 
7: import "../../challengeV2/EdgeChallengeManager.sol";

8: import "./IAbsBoldStakingPool.sol";
9: 
10: interface IEdgeStakingPool is IAbsBoldStakingPool {
  • IEdgeStakingPoolCreator.sol ( 5-7, 7-9 ):
5: pragma solidity ^0.8.0;
6: 
7: import "./IEdgeStakingPool.sol";

7: import "./IEdgeStakingPool.sol";
8: 
9: interface IEdgeStakingPoolCreator {
5: pragma solidity ^0.8.0;
6: 
7: import "./Messages.sol";

106:     }
107: 
108:     function isUpdatable(BufferData storage self) internal view returns (bool) {

113:     }
114: 
115:     function isValidBufferConfig(BufferConfig memory config) internal pure returns (bool) {
7: pragma experimental ABIEncoderV2;
8: 
9: import "../libraries/IGasRefunder.sol";

13: import "./DelayBufferTypes.sol";
14: 
15: interface ISequencerInbox is IDelayedMessageProvider {
5: pragma solidity ^0.8.0;
6: 
7: import {

106:     }
107: 
108:     modifier onlyRollupOwnerOrBatchPosterManager() {

155:     }
156: 
157:     function _chainIdChanged() internal view returns (bool) {

159:     }
160: 
161:     function postUpgradeInit(BufferConfig memory bufferConfig_)

175:     }
176: 
177:     function initialize(

214:     }
215: 
216:     function getTimeBounds() internal view virtual returns (IBridge.TimeBounds memory) {

242:     }
243: 
244:     function maxTimeVariation()

267:     }
268: 
269:     function maxTimeVariationInternal()

453:     }
454: 
455:     function addSequencerL2BatchFromBlobsImpl(

510:     }
511: 
512:     function addSequencerL2BatchFromCalldataImpl(

603:     }
604: 
605:     function delayProofImpl(uint256 afterDelayedMessagesRead, DelayProof memory delayProof)

626:     }
627: 
628:     function isDelayProofRequired(uint256 afterDelayedMessagesRead) internal view returns (bool) {

635:     }
636: 
637:     function packHeader(uint256 afterDelayedMessagesRead)

789:     }
790: 
791:     function addSequencerL2BatchImpl(

822:     }
823: 
824:     function inboxAccs(uint256 index) external view returns (bytes32) {

826:     }
827: 
828:     function batchCount() external view returns (uint256) {

845:     }
846: 
847:     function _setBufferConfig(BufferConfig memory bufferConfig_) internal {

865:     }
866: 
867:     function _setMaxTimeVariation(ISequencerInbox.MaxTimeVariation memory maxTimeVariation_)

945:     }
946: 
947:     function setBufferConfig(BufferConfig memory bufferConfig_) external onlyRollupOwner {

950:     }
951: 
952:     function isValidKeysetHash(bytes32 ksHash) external view returns (bool) {
  • EdgeChallengeManager.sol ( 5-7 ):
5: pragma solidity ^0.8.17;
6: 
7: import "../rollup/Assertion.sol";
  • IAssertionChain.sol ( 5-7 ):
5: pragma solidity ^0.8.17;
6: 
7: import "../bridge/IBridge.sol";
5: pragma solidity ^0.8.17;
6: 
7: import "./Enums.sol";

65: }
66: 
67: library ChallengeEdgeLib {

182:     }
183: 
184:     function mutualIdMem(ChallengeEdge memory ce) internal pure returns (bytes32) {
  • ChallengeErrors.sol ( 5-7 ):
5: pragma solidity ^0.8.17;
6: 
7: import "./Enums.sol";
5: pragma solidity ^0.8.17;
6: 
7: import "./UintUtilsLib.sol";

486:     }
487: 
488:     function timeUnrivaledTotal(EdgeStore storage store, bytes32 edgeId) internal view returns (uint256) {

514:     }
515: 
516:     function updateTimerCacheByChildren(EdgeStore storage store, bytes32 edgeId) internal returns (bool, uint256) {

518:     }
519: 
520:     function updateTimerCacheByClaim(
  • MerkleTreeLib.sol ( 5-7 ):
5: pragma solidity ^0.8.17;
6: 
7: import "../../libraries/MerkleLib.sol";
5: pragma solidity ^0.8.0;
6: 
7: import "./AssertionState.sol";

91:     }
92: 
93:     function requireExists(AssertionNode memory self) internal pure {
5: pragma solidity ^0.8.0;
6: 
7: import "../state/GlobalState.sol";

15: }
16: 
17: library AssertionStateLib {

20:     }
21: 
22:     function hash(AssertionState memory state) internal pure returns (bytes32) {
5: pragma solidity ^0.8.0;
6: 
7: import "@openzeppelin/contracts-upgradeable/utils/Create2Upgradeable.sol";

47: }
48: 
49: interface IOldRollup {

75: }
76: 
77: interface IOldRollupAdmin {

81: }
82: 
83: interface ISeqInboxPostUpgradeInit {

104:     }
105: 
106:     function set(bytes32 h, ExecutionState calldata executionState, uint256 inboxMaxCount) public {

110:     }
111: 
112:     function get(bytes32 h) public view returns (ExecutionState memory executionState, uint256 inboxMaxCount) {

128:     }
129: 
130:     function wasmModuleRoot() external view returns (bytes32) {

132:     }
133: 
134:     function latestConfirmed() external view returns (uint64) {

136:     }
137: 
138:     function getNode(uint64 nodeNum) external view returns (Node memory) {

140:     }
141: 
142:     function getStakerAddress(uint64 stakerNum) external view returns (address) {

144:     }
145: 
146:     function stakerCount() external view returns (uint64) {

148:     }
149: 
150:     function getStaker(address staker) external view returns (OldStaker memory) {

152:     }
153: 
154:     function isValidator(address validator) external view returns (bool) {

156:     }
157: 
158:     function validatorWalletCreator() external view returns (address) {

171:     }
172: 
173:     function array() public view returns (uint256[] memory) {

409:     }
410: 
411:     function upgradeSurroundingContracts(address newRollupAddress) private {

431:     }
432: 
433:     function upgradeSequencerInbox() private {

462:     }
463: 
464:     function perform(address[] memory validators) external {
5: pragma solidity ^0.8.0;
6: 
7: import "../bridge/Bridge.sol";

19: import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
20: 
21: contract BridgeCreator is Ownable {

51:     }
52: 
53:     function updateTemplates(BridgeTemplates calldata _newTemplates) external onlyOwner {

56:     }
57: 
58:     function updateERC20Templates(BridgeTemplates calldata _newTemplates) external onlyOwner {

61:     }
62: 
63:     function _createBridge(

97:     }
98: 
99:     function createBridge(
  • Config.sol ( 5-7 ):
5: pragma solidity ^0.8.0;
6: 
7: import "../state/GlobalState.sol";
5: pragma solidity ^0.8.0;
6: 
7: import "./IRollupCore.sol";

11: import "./Config.sol";
12: 
13: interface IRollupAdmin {
5: pragma solidity ^0.8.0;
6: 
7: import "./Assertion.sol";

13: import "../challengeV2/IAssertionChain.sol";
14: 
15: interface IRollupCore is IAssertionChain {
5: pragma solidity ^0.8.0;
6: 
7: import "./IRollupCore.sol";

10: import "../bridge/IOwnable.sol";
11: 
12: interface IRollupUser is IRollupCore, IOwnable {
5: pragma solidity ^0.8.0;
6: 
7: import "./IRollupAdmin.sol";

13: import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
14: 
15: contract RollupAdminLogic is RollupCore, IRollupAdmin, DoubleLogicUUPSUpgradeable {

226:     }
227: 
228:     function forceRefundStaker(address[] calldata staker) external override whenPaused {

235:     }
236: 
237:     function forceCreateAssertion(

256:     }
257: 
258:     function forceConfirmAssertion(

267:     }
268: 
269:     function setLoserStakeEscrow(address newLoserStakerEscrow) external override {
5: pragma solidity ^0.8.0;
6: 
7: import "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";

20: import "../libraries/ArbitrumChecker.sol";
21: 
22: abstract contract RollupCore is IRollupCore, PausableUpgradeable {

363:     }
364: 
365:     function createNewAssertion(

518:     }
519: 
520:     function genesisAssertionHash() external pure returns (bytes32) {

530:     }
531: 
532:     function getFirstChildCreationBlock(bytes32 assertionHash) external view returns (uint64) {

534:     }
535: 
536:     function getSecondChildCreationBlock(bytes32 assertionHash) external view returns (uint64) {

538:     }
539: 
540:     function validateAssertionHash(

547:     }
548: 
549:     function validateConfig(bytes32 assertionHash, ConfigData calldata configData) external view {

551:     }
552: 
553:     function isFirstChild(bytes32 assertionHash) external view returns (bool) {

555:     }
556: 
557:     function isPending(bytes32 assertionHash) external view returns (bool) {
5: pragma solidity ^0.8.0;
6: 
7: import "./RollupProxy.sol";

15: import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
16: 
17: contract RollupCreator is Ownable {

61:     receive() external payable {}
62: 
63:     function setTemplates(

265:     }
266: 
267:     function _deployUpgradeExecutor(address rollupOwner, ProxyAdmin proxyAdmin)

285:     }
286: 
287:     function _deployFactories(
5: pragma solidity ^0.8.0;
6: 
7: import "../state/GlobalState.sol";

15: import "../challengeV2/EdgeChallengeManager.sol";
16: 
17: library RollupLib {

71:     }
72: 
73:     function validateConfigHash(
5: pragma solidity ^0.8.0;
6: 
7: import "../libraries/AdminFallbackProxy.sol";

9: import "./Config.sol";
10: 
11: contract RollupProxy is AdminFallbackProxy {
5: pragma solidity ^0.8.0;
6: 
7: import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

13: import {ETH_POS_BLOCK_TIME} from "../libraries/Constants.sol";
14: 
15: contract RollupUserLogic is RollupCore, UUPSNotUpgradeable, IRollupUser {

60:     }
61: 
62:     function removeWhitelistAfterFork() external {

306:     }
307: 
308:     function owner() external view returns (address) {

364:     }
365: 
366:     function receiveTokens(uint256 tokenAmount) private {
</details>

[N-67] Consider adding formal verification proofs

Formal verification is the act of proving or disproving the correctness of intended algorithms underlying a system with respect to a certain formal specification/property/invariant, using formal methods of mathematics.

Some tools that are currently available to perform these tests on smart contracts are SMTChecker and Certora Prover.

<details> <summary>There are 39 instances (click to show):</summary> </details>

[N-68] Use scopes sparingly

The use of scoped blocks, denoted by {} without a preceding control structure like if, for, etc., allows for the creation of isolated scopes within a function. While this can be useful for managing memory and preventing naming conflicts, it should be used sparingly. Excessive use of these scope blocks can obscure the code's logic flow and make it more difficult to understand, impeding code maintainability. As a best practice, only employ scoped blocks when necessary for memory management or to avoid clear naming conflicts. Otherwise, aim for clarity and simplicity in your code structure for optimal readability and maintainability.

There are 6 instances:

622:         {
623:             (bytes32[] memory preExpansion, bytes32[] memory proof) = abi.decode(prefixProof, (bytes32[], bytes32[]));

631:         {
632:             // midpoint proof it valid, create and store the children

645:         {
646:             ChallengeEdge memory upperChild = ChallengeEdgeLib.newChildEdge(

797:         {
798:             bytes32 cursor = edgeId;
405:         {
406:             // This new assertion consumes the messages from prevInboxPosition to afterInboxPosition
142:         {
143:             // Make sure the immutable maxDataSize is as expected

#0 - c4-judge

2024-06-05T11:40:58Z

Picodes marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter