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: 27/33
Findings: 1
Award: $813.40
🌟 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
813.4016 USDC - $813.40
Issue | Instances | |
---|---|---|
[L-01] | Missing address(0) checks when assigning to address state variables | 6 |
[L-02] | Division before multiplications leads to precision loss | 1 |
[L-03] | initialize can be frontrun | 5 |
[L-04] | Potential division by zero | 4 |
[L-05] | Possible loss of precision | 4 |
[L-06] | Setter functions lacking input sanitization | 1 |
[L-07] | Solidity version 0.8.20 may not work on other chains due to PUSH0 | 3 |
[L-08] | Use two-step ownership transfers | 1 |
[L-09] | Upgradeable contracts should have a storage gap __gap[n] | 1 |
[L-10] | Missing address(0) checks in constructor/initialize | 13 |
Total issues: 10
Total instances: 39
Â
Issue | Instances | |
---|---|---|
[N-01] | Comparisons should place constants on the left hand side | 4 |
[N-02] | Duplicated require /revert checks should be refactored to a modifier | 1 |
[N-03] | Event missing msg.sender parameter | 2 |
[N-04] | Use indexed for event parameters | 7 |
[N-05] | Function names should use mixedCase | 1 |
[N-06] | Function order doesn't follow Solidity style guide | 3 |
[N-07] | Remove internal functions that are not called | 9 |
[N-08] | Use constants rather than magic numbers | 6 |
[N-09] | Use named parameters for mappings | 4 |
[N-10] | NatSpec @param missing | 2 |
[N-11] | NatSpec @return missing | 1 |
[N-12] | Non-external variable names should begin with an underscore | 4 |
[N-13] | public functions not called internally should be declared external | 5 |
[N-14] | Use a more recent version of Solidity | 3 |
[N-15] | Implementing renounceOwnership is dangerous | 1 |
[N-16] | Use single file for all system-wide constants | 8 |
[N-17] | Large numeric literals should use underscores | 1 |
[N-18] | Use delete rather than assigning to 0 | 1 |
[N-19] | Non constant state variables should be named using mixedCase | 12 |
[N-20] | Upper case variable names should be reserved for constant variables | 9 |
Total issues: 20
Total instances: 84
Â
address(0)
checks when assigning to address
state variablesFile: ../contracts/../contracts/L1/L1CrossDomainMessenger.sol 31: PORTAL = _portal;
File: ../contracts/../contracts/L1/OptimismPortal.sol 156: L2_ORACLE = _l2Oracle; 157: GUARDIAN = _guardian; 158: SYSTEM_CONFIG = _config;
</details>File: ../contracts/../contracts/L1/L2OutputOracle.sol 107: PROPOSER = _proposer; 108: CHALLENGER = _challenger;
Â
Since Solidity cannot deal with decimal values, make sure to use multiplication before division to avoid precision loss due to rounding errors.
<details><summary>Instances: 1</summary></details>File: ../contracts/../contracts/L1/ResourceMetering.sol 105: int256 baseFeeDelta = (int256(uint256(params.prevBaseFee)) * gasUsedDelta) / 106: (targetResourceLimit * int256(uint256(config.baseFeeMaxChangeDenominator)));
Â
initialize
can be frontrunLack of access control on initialize
function means that it can be frontrun,
allowing a malicious user to steal ownership of the contract and necessitating
an expensive re-deployment.
File: ../contracts/../contracts/L1/SystemConfig.sol 125: function initialize( 126: address _owner, 127: uint256 _overhead, 128: uint256 _scalar, 129: bytes32 _batcherHash, 130: uint64 _gasLimit, 131: address _unsafeBlockSigner, 132: ResourceMetering.ResourceConfig memory _config 133: ) public initializer {
File: ../contracts/../contracts/L1/L1CrossDomainMessenger.sol 38: function initialize() public initializer {
File: ../contracts/../contracts/L1/OptimismPortal.sol 165: function initialize(bool _paused) public initializer {
File: ../contracts/../contracts/L1/L2OutputOracle.sol 120: function initialize(uint256 _startingBlockNumber, uint256 _startingTimestamp) 121: public 122: initializer 123: {
</details>File: ../contracts/../contracts/L2/L2CrossDomainMessenger.sol 34: function initialize() public initializer {
Â
When dividing by a value that may be zero, add checks so that execution does not unexpectedly revert.
<details><summary>Instances: 4</summary>File: ../contracts/../contracts/L1/SystemConfig.sol 289: require( 290: ((_config.maxResourceLimit / _config.elasticityMultiplier) * 291: _config.elasticityMultiplier) == _config.maxResourceLimit, 292: "SystemConfig: precision loss with target resource limit" 293: );
File: ../contracts/../contracts/L1/ResourceMetering.sol 97: int256 targetResourceLimit = int256(uint256(config.maxResourceLimit)) / 98: int256(uint256(config.elasticityMultiplier)); 155: uint256 gasCost = resourceCost / Math.max(block.basefee, 1 gwei);
</details>File: ../contracts/../contracts/L2/GasPriceOracle.sol 48: uint256 scaled = unscaled / divisor;
Â
Division by large numbers may result in precision loss due to rounding down, or even the result being erroneously equal to zero. Consider adding checks on the numerator to ensure precision loss is handled appropriately.
<details><summary>Instances: 4</summary>File: ../contracts/../contracts/L1/SystemConfig.sol 289: require( 290: ((_config.maxResourceLimit / _config.elasticityMultiplier) * 291: _config.elasticityMultiplier) == _config.maxResourceLimit, 292: "SystemConfig: precision loss with target resource limit" 293: );
File: ../contracts/../contracts/L1/ResourceMetering.sol 97: int256 targetResourceLimit = int256(uint256(config.maxResourceLimit)) / 98: int256(uint256(config.elasticityMultiplier)); 155: uint256 gasCost = resourceCost / Math.max(block.basefee, 1 gwei);
</details>File: ../contracts/../contracts/L2/GasPriceOracle.sol 48: uint256 scaled = unscaled / divisor;
Â
Setter functions should have input sanitization. A malicious/compromised owner, or one that erroneously does a "fat finger", can change key variables to extreme values that result in DoS, or even loss of funds.
<details><summary>Instances: 1</summary></details>File: ../contracts/../contracts/L1/SystemConfig.sol 192: function setBatcherHash(bytes32 _batcherHash) external onlyOwner {
Â
PUSH0
The compiler for Solidity 0.8.20 switches the default target EVM version to Shanghai, which includes the new PUSH0 op code. This op code may not yet be implemented on all L2s, so deployment on these chains will fail. To work around this issue, use an earlier EVM version.
<details><summary>Instances: 3</summary>File: ../contracts/../contracts/L2/CrossDomainOwnable2.sol 2: pragma solidity ^0.8.0;
File: ../contracts/../contracts/L2/CrossDomainOwnable.sol 2: pragma solidity ^0.8.0;
</details>File: ../contracts/../contracts/L2/CrossDomainOwnable3.sol 2: pragma solidity ^0.8.0;
Â
The current ownership transfer process involves the current owner calling transferOwnership()
. This function checks the new owner is not the zero address and proceeds to write the new owner's address into the owner's state variable.
If the nominated EOA account is not a valid account, it is entirely possible the owner may accidentally transfer ownership to an uncontrolled account, breaking all functions with the onlyOwner() modifier.
Consider implementing a two step process where the owner nominates an account and the nominated account needs to call an acceptOwnership() function for the transfer of ownership to fully succeed.
This can be achieved using OpenZeppelin's Ownable2Step
or Ownable2StepUpgradeable
.
</details>File: ../contracts/../contracts/L1/SystemConfig.sol 16: contract SystemConfig is OwnableUpgradeable, Semver {
Â
__gap[n]
A storage gap is an empty fixed length array conventionally added at the end of an upgradeable contracts storage, to reserve space for state variables to be added in the future without compromising storage compatibility. See here for more information.
<details><summary>Instances: 1</summary></details>File: ../contracts/../contracts/L1/SystemConfig.sol 16: contract SystemConfig is OwnableUpgradeable, Semver {
Â
address(0)
checks in constructor/initializeFailing to check for invalid parameters on deployment may result in an erroneous input and require an expensive redeployment.
<details><summary>Instances: 13</summary>File: ../contracts/../contracts/L1/SystemConfig.sol 93: constructor( 94: address _owner, 95: uint256 _overhead, 96: uint256 _scalar, 97: bytes32 _batcherHash, 98: uint64 _gasLimit, 99: address _unsafeBlockSigner, 100: ResourceMetering.ResourceConfig memory _config 101: ) Semver(1, 3, 0) { 125: function initialize( 126: address _owner, 127: uint256 _overhead, 128: uint256 _scalar, 129: bytes32 _batcherHash, 130: uint64 _gasLimit, 131: address _unsafeBlockSigner, 132: ResourceMetering.ResourceConfig memory _config 133: ) public initializer {
File: ../contracts/../contracts/L1/L1CrossDomainMessenger.sol 27: constructor(OptimismPortal _portal) 28: Semver(1, 4, 0) 29: CrossDomainMessenger(Predeploys.L2_CROSS_DOMAIN_MESSENGER) 30: {
File: ../contracts/../contracts/L1/L1ERC721Bridge.sol 28: constructor(address _messenger, address _otherBridge) 29: Semver(1, 1, 1) 30: ERC721Bridge(_messenger, _otherBridge) 31: {}
File: ../contracts/../contracts/L1/OptimismPortal.sol 150: constructor( 151: L2OutputOracle _l2Oracle, 152: address _guardian, 153: bool _paused, 154: SystemConfig _config 155: ) Semver(1, 6, 0) {
File: ../contracts/../contracts/L1/L1StandardBridge.sol 98: constructor(address payable _messenger) 99: Semver(1, 1, 0) 100: StandardBridge(_messenger, payable(Predeploys.L2_STANDARD_BRIDGE)) 101: {}
File: ../contracts/../contracts/L1/L2OutputOracle.sol 90: constructor( 91: uint256 _submissionInterval, 92: uint256 _l2BlockTime, 93: uint256 _startingBlockNumber, 94: uint256 _startingTimestamp, 95: address _proposer, 96: address _challenger, 97: uint256 _finalizationPeriodSeconds 98: ) Semver(1, 3, 0) {
File: ../contracts/../contracts/L2/L2CrossDomainMessenger.sol 24: constructor(address _l1CrossDomainMessenger) 25: Semver(1, 4, 0) 26: CrossDomainMessenger(_l1CrossDomainMessenger) 27: {
File: ../contracts/../contracts/L2/SequencerFeeVault.sol 20: constructor(address _recipient) FeeVault(_recipient, 10 ether) Semver(1, 1, 0) {}
File: ../contracts/../contracts/L2/L2ERC721Bridge.sol 28: constructor(address _messenger, address _otherBridge) 29: Semver(1, 1, 0) 30: ERC721Bridge(_messenger, _otherBridge) 31: {}
File: ../contracts/../contracts/L2/L2StandardBridge.sol 66: constructor(address payable _otherBridge) 67: Semver(1, 1, 0) 68: StandardBridge(payable(Predeploys.L2_CROSS_DOMAIN_MESSENGER), _otherBridge) 69: {}
File: ../contracts/../contracts/L2/L1FeeVault.sol 19: constructor(address _recipient) FeeVault(_recipient, 10 ether) Semver(1, 1, 0) {}
</details>File: ../contracts/../contracts/L2/BaseFeeVault.sol 19: constructor(address _recipient) FeeVault(_recipient, 10 ether) Semver(1, 1, 0) {}
Â
This practise avoids typo errors.
<details> <summary>Instances: 4</summary>File: ../contracts/../contracts/L1/OptimismPortal.sol 277: provenWithdrawal.timestamp == 0 || 345: provenWithdrawal.timestamp != 0, 461: require(_data.length <= 120_000, "OptimismPortal: data too large");
</details>File: ../contracts/../contracts/L1/L2OutputOracle.sol 326: l2Outputs.length == 0
Â
require
/revert
checks should be refactored to a modifieror function
The compiler will inline the function, which will avoid JUMP
instructions
usually associated with functions.
</details>File: ../contracts/../contracts/L1/SystemConfig.sol 142: require(_gasLimit >= minimumGasLimit(), "SystemConfig: gas limit too low"); 219: require(_gasLimit >= minimumGasLimit(), "SystemConfig: gas limit too low");
Â
msg.sender
parameterWhen 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>File: ../contracts/../contracts/L1/L2OutputOracle.sol 166: emit OutputsDeleted(prevNextL2OutputIndex, _l2OutputIndex); 220: emit OutputProposed(_outputRoot, nextOutputIndex(), _l2BlockNumber, block.timestamp);
Â
indexed
for event parametersIndex event fields make the field more quickly accessible to off-chain tools that parse events. This is especially useful when it comes to filtering based on an address. However, note that each index field costs extra gas during emission, so it's not necessarily best to index the maximum allowed per event (three fields).
Where applicable, each event should use three indexed
fields if there are three
or more fields, and gas usage is not particularly of concern for the events in
question. If there are fewer than three applicable fields, all of the applicable
fields should be indexed.
File: ../contracts/../contracts/L1/SystemConfig.sol 80: event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data);
File: ../contracts/../contracts/L1/OptimismPortal.sol 118: event WithdrawalFinalized(bytes32 indexed withdrawalHash, bool success); 125: event Paused(address account); 132: event Unpaused(address account);
File: ../contracts/../contracts/L1/L1StandardBridge.sol 30: event ETHDepositInitiated( 31: address indexed from, 32: address indexed to, 33: uint256 amount, 34: bytes extraData 35: ); 46: event ETHWithdrawalFinalized( 47: address indexed from, 48: address indexed to, 49: uint256 amount, 50: bytes extraData 51: );
</details>File: ../contracts/../contracts/L2/CrossDomainOwnable3.sol 26: event OwnershipTransferred( 27: address indexed previousOwner, 28: address indexed newOwner, 29: bool isLocal 30: );
Â
See the Solidity style guide for more info.
<details> <summary>Instances: 1</summary></details>File: ../contracts/../contracts/L1/ResourceMetering.sol 179: function __ResourceMetering_init() internal onlyInitializing {
Â
The Solidity style guide
states that functions should be laid out in the following order: constructor
,
receive
, fallback
, external
, public
, internal
, private
. In the
following contracts, this is not the case.
File: ../contracts/../contracts/L1/SystemConfig.sol 16: contract SystemConfig is OwnableUpgradeable, Semver {
File: ../contracts/../contracts/L1/OptimismPortal.sol 23: contract OptimismPortal is Initializable, ResourceMetering, Semver {
</details>File: ../contracts/../contracts/L1/L2OutputOracle.sol 15: contract L2OutputOracle is Initializable, Semver {
Â
internal
functions that are not calledFile: ../contracts/../contracts/L1/L1CrossDomainMessenger.sol 45: function _sendMessage( 57: function _isOtherMessenger() internal view override returns (bool) { 64: function _isUnsafeTarget(address _target) internal view override returns (bool) {
File: ../contracts/../contracts/L1/L1ERC721Bridge.sol 77: function _initiateBridgeERC721(
File: ../contracts/../contracts/L1/OptimismPortal.sol 225: function _resourceConfig()
File: ../contracts/../contracts/L2/L2CrossDomainMessenger.sol 51: function _sendMessage( 65: function _isOtherMessenger() internal view override returns (bool) { 72: function _isUnsafeTarget(address _target) internal view override returns (bool) {
</details>File: ../contracts/../contracts/L2/L2ERC721Bridge.sol 79: function _initiateBridgeERC721(
Â
Improves code readability and reduces margin for error.
<details> <summary>Instances: 6</summary>File: ../contracts/../contracts/L1/OptimismPortal.sol 197: return _byteCount * 16 + 21000; 461: require(_data.length <= 120_000, "OptimismPortal: data too large");
</details>File: ../contracts/../contracts/L2/GasPriceOracle.sol 46: uint256 divisor = 10**DECIMALS; 122: total += 4; 124: total += 16; 128: return unsigned + (68 * 16);
Â
Consider using named parameters in mappings (e.g. mapping(address account => uint256 balance)
) to improve readability. This feature is present since Solidity 0.8.18.
File: ../contracts/../contracts/L1/L1ERC721Bridge.sol 20: mapping(address => mapping(address => mapping(uint256 => bool))) public deposits;
File: ../contracts/../contracts/L1/OptimismPortal.sol 72: mapping(bytes32 => bool) public finalizedWithdrawals; 77: mapping(bytes32 => ProvenWithdrawal) public provenWithdrawals;
</details>File: ../contracts/../contracts/L2/L2ToL1MessagePasser.sol 32: mapping(bytes32 => bool) public sentMessages;
Â
@param
missing</details>File: ../contracts/../contracts/L1/OptimismPortal.sol 162: /** 163: * @notice Initializer. 164: */ 165: function initialize(bool _paused) public initializer { 189: /** 190: * @notice Computes the minimum gas limit for a deposit. The minimum gas limit 191: * linearly increases based on the size of the calldata. This is to prevent 192: * users from creating L2 resource usage without paying for it. This function 193: * can be used when interacting with the portal to ensure forwards compatibility. 194: * 195: */ 196: function minimumGasLimit(uint64 _byteCount) public pure returns (uint64) {
Â
@return
missing</details>File: ../contracts/../contracts/L1/OptimismPortal.sol 189: /** 190: * @notice Computes the minimum gas limit for a deposit. The minimum gas limit 191: * linearly increases based on the size of the calldata. This is to prevent 192: * users from creating L2 resource usage without paying for it. This function 193: * can be used when interacting with the portal to ensure forwards compatibility. 194: * 195: */ 196: function minimumGasLimit(uint64 _byteCount) public pure returns (uint64) {
Â
According to the Solidity Style Guide, Non-external variable and function names should begin with an underscore.
<details> <summary>Instances: 4</summary>File: ../contracts/../contracts/L1/OptimismPortal.sol 40: uint256 internal constant DEPOSIT_VERSION = 0; 45: uint64 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000;
</details>File: ../contracts/../contracts/L2/L2ToL1MessagePasser.sol 22: uint256 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000; 37: uint240 internal msgNonce;
Â
public
functions not called internally should be declared external
Contracts are allowed to override their parents' functions and change the visibility from external to public.
<details> <summary>Instances: 5</summary>File: ../contracts/../contracts/L2/L2CrossDomainMessenger.sol 44: function l1CrossDomainMessenger() public view returns (address) {
File: ../contracts/../contracts/L2/GasPriceOracle.sol 57: function gasPrice() public view returns (uint256) { 66: function baseFee() public view returns (uint256) { 103: function decimals() public pure returns (uint256) {
</details>File: ../contracts/../contracts/L2/SequencerFeeVault.sol 28: function l1FeeWallet() public view returns (address) {
Â
Use a Solidity version of at least 0.8.2 to get simple compiler automatic inlining.
Use a Solidity version of at least 0.8.3 to get better struct packing and cheaper multiple storage reads.
Use a Solidity version of at least 0.8.4 to get custom errors, which are cheaper at deployment than revert()
/require()
strings.
Use a Solidity version of at least 0.8.10 to have external calls skip contract existence checks if the external call has a return value.
Use a Solidity version of at least 0.8.12 to get string.concat() to be used instead of abi.encodePacked(<str>,<str>).
Use a solidity version of at least 0.8.13 to get the ability to use using for with a list of free functions.
<details> <summary>Instances: 3</summary>File: ../contracts/../contracts/L2/CrossDomainOwnable2.sol 2: pragma solidity ^0.8.0;
File: ../contracts/../contracts/L2/CrossDomainOwnable.sol 2: pragma solidity ^0.8.0;
</details>File: ../contracts/../contracts/L2/CrossDomainOwnable3.sol 2: pragma solidity ^0.8.0;
Â
renounceOwnership
is dangerousTypically, the contract's owner is the account that deploys the contract. As a result, the owner is able to perform certain privileged activities.
The OpenZeppelin's Ownable used in this project contract implements renounceOwnership. This can represent a certain risk if the ownership is renounced for any other reason than by design.
Renouncing ownership will leave the contract without an owner, thereby removing any functionality that is only available to the owner.
It is recommended to either reimplement the function to disable it, or to clearly specify if it is part of the contract design.
<details> <summary>Instances: 1</summary></details>File: ../contracts/../contracts/L1/SystemConfig.sol 16: contract SystemConfig is OwnableUpgradeable, Semver {
Â
File: ../contracts/../contracts/L1/SystemConfig.sol 36: uint256 public constant VERSION = 0; 43: bytes32 public constant UNSAFE_BLOCK_SIGNER_SLOT = keccak256("systemconfig.unsafeblocksigner");
File: ../contracts/../contracts/L1/OptimismPortal.sol 40: uint256 internal constant DEPOSIT_VERSION = 0; 45: uint64 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000;
File: ../contracts/../contracts/L2/GasPriceOracle.sol 28: uint256 public constant DECIMALS = 6;
File: ../contracts/../contracts/L2/L1Block.sol 19: address public constant DEPOSITOR_ACCOUNT = 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001;
</details>File: ../contracts/../contracts/L2/L2ToL1MessagePasser.sol 22: uint256 internal constant RECEIVE_DEFAULT_GAS_LIMIT = 100_000; 27: uint16 public constant MESSAGE_VERSION = 1;
Â
</details>File: ../contracts/../contracts/L1/OptimismPortal.sol 197: return _byteCount * 16 + 21000;
Â
delete
rather than assigning to 0
Using delete
more closely aligns with the intention of the action, and draws more
attention towards the changing of state, which may lead to a more thorough audit of
its associated logic"
</details>File: ../contracts/../contracts/L1/ResourceMetering.sol 136: params.prevBoughtGas = 0;
Â
constant
state variables should be named using mixedCaseSee the Solidity style guide for more info.
<details> <summary>Instances: 12</summary>File: ../contracts/../contracts/L1/SystemConfig.sol 27: BATCHER, 28: GAS_CONFIG, 29: GAS_LIMIT, 30: UNSAFE_BLOCK_SIGNER 31: } 71: ResourceMetering.ResourceConfig internal _resourceConfig;
File: ../contracts/../contracts/L1/L1CrossDomainMessenger.sol 20: OptimismPortal public immutable PORTAL;
File: ../contracts/../contracts/L1/ResourceMetering.sol 68: uint256[48] private __gap;
File: ../contracts/../contracts/L1/OptimismPortal.sol 50: L2OutputOracle public immutable L2_ORACLE; 55: SystemConfig public immutable SYSTEM_CONFIG; 60: address public immutable GUARDIAN;
</details>File: ../contracts/../contracts/L1/L2OutputOracle.sol 20: uint256 public immutable SUBMISSION_INTERVAL; 25: uint256 public immutable L2_BLOCK_TIME; 30: address public immutable CHALLENGER; 35: address public immutable PROPOSER; 40: uint256 public immutable FINALIZATION_PERIOD_SECONDS;
Â
constant
variablesSee the Solidity style guide for more info.
<details> <summary>Instances: 9</summary>File: ../contracts/../contracts/L1/L1CrossDomainMessenger.sol 20: OptimismPortal public immutable PORTAL;
File: ../contracts/../contracts/L1/OptimismPortal.sol 50: L2OutputOracle public immutable L2_ORACLE; 55: SystemConfig public immutable SYSTEM_CONFIG; 60: address public immutable GUARDIAN;
</details>File: ../contracts/../contracts/L1/L2OutputOracle.sol 20: uint256 public immutable SUBMISSION_INTERVAL; 25: uint256 public immutable L2_BLOCK_TIME; 30: address public immutable CHALLENGER; 35: address public immutable PROPOSER; 40: uint256 public immutable FINALIZATION_PERIOD_SECONDS;
Â
#0 - c4-judge
2023-06-16T14:54:15Z
0xleastwood marked the issue as grade-b