Platform: Code4rena
Start Date: 26/05/2023
Pot Size: $100,000 USDC
Total HM: 0
Participants: 33
Period: 14 days
Judge: leastwood
Id: 241
League: ETH
Rank: 7/33
Findings: 1
Award: $8,029.15
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: rbserver
Also found by: 0x73696d616f, 0xTheC0der, 0xdeadbeef, 0xhacksmithh, Bauchibred, GalloDaSballo, KKat7531, Madalad, MohammedRizwan, Rolezn, SAAJ, SanketKogekar, Sathish9098, VictoryGod, brgltd, btk, codeslide, descharre, hunter_w3b, jauvany, kaveyjoe, ladboy233, nadin, niser93, shealtielanz, souilos, trysam2003, yongskiws
8029.1527 USDC - $8,029.15
Low Risk & Non Critical Findings
Low-Risk Findings List
Number | Issue Details | Instances |
---|---|---|
L-01 | Missing storage gaps in Implementation contracts. | 5 |
L-02 | Missing zero address checks on Critical State changing parameters. | 2 in all Proxies |
L-03 | No need for a redundant Function where the Function createStandardL2Token() is redundant and might express a lack of trustworthiness of the protocol. | 1 |
L-04 | Missing non-reentrant modifier on Critical function | 2 |
L-05 | Ownership maybe be lost. | 2 |
L-06 | Unsafe Down-cast. | 1 |
L-07 | Unbounded Loops may result in DOS . | 1 |
L-08 | Critical Implementations Initializers not Locked . | 5 |
L-09 | The onlyEOA modifier can be bypassed. | 1 |
L -10 | Loss of Precision . | 1 |
L -11 | Division by Zero not prevented. | 2 |
L -12 | Missing checks for the basefee and l1FeeScalar input values. | 1 |
Total ~ 12 Issues
storage gaps
in Implementation contracts.See this link for a description of this storage variable. While some contracts may not currently be sub-classed, adding the variable now protects against forgetting to add it in the future.
zero address
checks on Critical State
changing parametersZero-address
checks are a best-practice for input validation of critical address parameters. While the codebase applies this to most addresses in setters, there are many places where this is missing in mostly in the proxy contracts.
_changeAdmin in the proxy contracts.
function _changeAdmin(address _admin) internal { address previous = _getAdmin(); assembly { sstore(OWNER_KEY, _admin) } emit AdminChanged(previous, _admin); }
_setImplementation in the proxy contracts.
function _setImplementation(address _implementation) internal { assembly { sstore(IMPLEMENTATION_KEY, _implementation) } emit Upgraded(_implementation); }
redundant
Function where the Function createStandardL2Token()
is redundant and might express a lack of trustworthiness of the protocol.I understand the developers love an intuitive functions name and it's cool, but the function createStandardL2Token()
's only logic is to call the function createOptimismMintableERC20()
which creates an instance of the OptimismMintableERC20 contract, but an issue results from the function being called that is the createOptimismMintableERC20()
function having its visibility set to public
which means it can also be called externally.
this creates confusion for the external viewer as to why two functions will perform the same task.
This in turn will reflect on the trustworthiness of the whole protocol.
function createStandardL2Token( address _remoteToken, string memory _name, string memory _symbol ) external returns (address) { return createOptimismMintableERC20(_remoteToken, _name, _symbol); }
function createOptimismMintableERC20( address _remoteToken, string memory _name, string memory _symbol ) public returns (address) { //more code ... }
I recommended changing the name of the createOptimismMintableERC20()
function to be more intuitive and making it the sole function to be called for creating an instance of the OptimismMintableERC20
contract, thereby making the code simple and more trustworthy.
non-reentrant
modifier on Critical function.A critical function missing a non-reentrant modifier could cause the contract to be vulnerable to reentracy which could cause a lot of harm if exploited by an attacker.
L2StandardBridge.sol
.function withdraw( address _l2Token, uint256 _amount, uint32 _minGasLimit, bytes calldata _extraData ) external payable virtual onlyEOA { _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _minGasLimit, _extraData); }
Ownership
maybe be lost.Ownable2Step and Ownable2StepUpgradeable prevent the contract ownership from mistakenly being transferred to an address that cannot handle it (e.g. due to a typo in the address), by requiring that the recipient of the owner permissions actively accept via a contract call of its own.
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
Unsafe
Down-castWhen a type is downcast to a smaller type, the higher-order bits are truncated, effectively applying a modulo to the original value. Without any other checks, this wrapping will lead to unexpected behavior and bugs
minimumGasLimit() function SystemConfig.sol
return uint64(_resourceConfig.maxResourceLimit) + uint64(_resourceConfig.systemTxMaxGas);
Unbounded Loops
may result in DOS
Consider limiting the number of iterations in for-loops that can be called by anyone as they could call with an arbitrary amount of input causing the contract to surpass the block gas limit.
getL1GasUsed
function in GasPriceOracle.sol
function getL1GasUsed(bytes memory _data) public view returns (uint256) { uint256 total = 0; uint256 length = _data.length; for (uint256 i = 0; i < length; i++) { if (_data[i] == 0) { total += 4; } else { total += 16; } } uint256 unsigned = total + overhead(); return unsigned + (68 * 16); } }
Implementations
Initializers not Locked
.OpenZeppelin’s best practice is to lock the Initializers in the function by calling the _disableintializers
function in the contract but this isn't done here in this protocol.
onlyEOA
modifier can be bypassed.The onlyEOA modifier is used on a function to ensure that it cannot be called by a contract however this assumption is wrong due to the fact that the check of the modifier can be passed during the creation of a contract via its constructor, and could be exploited if it's unintended by the developers that a Smart Contract should call that function.
receive()
function in L2StandardBridge.sol
/** * @notice Allows EOAs to bridge ETH by sending directly to the bridge. */ receive() external payable override onlyEOA { _initiateWithdrawal( Predeploys.LEGACY_ERC20_ETH, msg.sender, msg.sender, msg.value, RECEIVE_DEFAULT_GAS_LIMIT, bytes("") ); }
add additional checks to ensure it's an EOA and be careful as to the implications.
Precision
.Division by large numbers may result in the result being zero, due to solidity not supporting fractions. Consider requiring a minimum amount for the numerator to ensure that it is always larger than the denominator
getL1Fee function in GasPriceOracle.sol
uint256 scaled = unscaled / divisor;
Zero
not prevented.The divisions below take an input parameter that does not have any zero-value checks, which may lead to the functions reverting when zero is passed.
getL1Fee function in GasPriceOracle.sol
uint256 scaled = unscaled / divisor;
_setResourceConfig function in the SystemConfig.sol
((_config.maxResourceLimit / _config.elasticityMultiplier) * _config.elasticityMultiplier) == _config.maxResourceLimit,
basefee
and l1FeeScalar
input values.The input values for the basefee
and l1FeeScalar
variables have no checks to ensure that their values are greater than zero. If this ever happens, it will lead to disastrous consequences which will affect all other functionalities that are dependent on these variables like the fee calculations.
function setL1BlockValues( uint64 _number, uint64 _timestamp, uint256 _basefee, bytes32 _hash, uint64 _sequenceNumber, bytes32 _batcherHash, uint256 _l1FeeOverhead, uint256 _l1FeeScalar ) external { require( msg.sender == DEPOSITOR_ACCOUNT, "L1Block: only the depositor account can set L1 block values" ); number = _number; timestamp = _timestamp; basefee = _basefee; hash = _hash; sequenceNumber = _sequenceNumber; batcherHash = _batcherHash; l1FeeOverhead = _l1FeeOverhead; l1FeeScalar = _l1FeeScalar; }
Non-Critical Findings
Non-Critical Issues List
Number | Issue Details | Instances |
---|---|---|
NC-01 | Consider Using Named Mappings | 2 |
NC-02 | Use constants instead of Magic Numbers | 3 |
NC-03 | Use Double Quote for string literals | 2 |
NC-04 | Use Immutable in place of constants | 1 |
Total ~ 4 Issues
Named Mapping
Consider moving to solidity version 0.8.18 or later, and using named mappings to make it easier to understand the purpose of each mapping.
mapping(bytes32 => bool) public finalizedWithdrawals; /** * @notice A mapping of withdrawal hashes to `ProvenWithdrawal` data. */ mapping(bytes32 => ProvenWithdrawal) public provenWithdrawals;
constants
instead of Magic Numbers
return _byteCount * 16 + 21000;
currentStep = 1; function step1() public onlyOwner step(1) function step2() public onlyOwner step(2)
Double Quote
for string literals.Best practice is to use double quotes for string literals rather than single literals.
* bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) * bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
Immutable
in place of constants
Expressions for constant values such as a call to keccak256()
, 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
.
bytes32 public constant UNSAFE_BLOCK_SIGNER_SLOT = keccak256("systemconfig.unsafeblocksigner");
#0 - c4-judge
2023-06-16T13:45:22Z
0xleastwood marked the issue as grade-a