Platform: Code4rena
Start Date: 22/09/2023
Pot Size: $100,000 USDC
Total HM: 15
Participants: 175
Period: 14 days
Judge: alcueca
Total Solo HM: 4
Id: 287
League: ETH
Rank: 48/175
Findings: 1
Award: $78.19
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: rvierdiiev
Also found by: 0x11singh99, 0xAnah, 0xta, Aamir, DavidGiladi, DevABDee, Eurovickk, JCK, K42, MrPotatoMagic, Pessimistic, Raihan, Rolezn, SM3_SS, SY_S, Sathish9098, Udsen, ayo_dev, blutorque, c3phas, clara, dharma09, hihen, hunter_w3b, jamshed, koxuan, lsaudit, marqymarq10, oualidpro, pfapostol, sivanesh_808, tabriz, wahedtalash77, zabihullahazadzoi, ziyou-
78.1884 USDC - $78.19
While we try our best to maintain readability in the provided code snippets, some functions have been truncated to highlight the affected portions.
It's important to note that during the implementation of these suggested changes, developers must exercise caution to avoid introducing vulnerabilities. Although the optimizations have been tested prior to these recommendations, it is the responsibility of the developers to conduct thorough testing again.
Code reviews and additional testing are strongly advised to minimize any potential risks associated with the refactoring process.
redeemDeposit
by refactoring the code to fail Early - 2.1k half of the time(scenario where sender is address(0))The developers have done an excellent job of identifying and implementing some of the most evident optimizations. However, we have identified additional areas where optimization is possible, some of which may not be immediately apparent.
Here, the storage variables can be tightly packed.
_unlocked
with bridgeAgentExecutorAddress
by reducing the size of _unlocked
to uint8
(Save 1 SLOT: 2K gas)File: /src/MulticallRootRouter.sol 76: /// @notice Bridge Agent Executor Address. 77: address public bridgeAgentExecutorAddress; 79: /// @notice Re-entrancy lock modifier state. 80: uint256 internal _unlocked = 1;
_unlocked
only has two possibilities , 1 or 2, as such uint8
should be more than enough to hold the value.
Reducing this size allows us to pack _unlocked
with bridgeAgentExecutorAddress
as they are being accessed together in some capacity which is shown below
The variable _unlocked
is being used by the modifier lock()
while the address variable bridgeAgentExecutorAddress
has been used on the internal function _requiresExecutor()
which is in turn used by the modifier requiresExecutor()
. Now both of this modifiers lock()
and requiresExecutor()
are used on the same functions eg https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/MulticallRootRouter.sol#L137
137: function execute(bytes calldata encodedData, uint16) external payable override lock requiresExecutor {
which means our two variables are being accessed in the same transaction and as such packing them would have a significant gas savings
diff --git a/src/MulticallRootRouter.sol b/src/MulticallRootRouter.sol index 0621f81..5c19ece 100644 --- a/src/MulticallRootRouter.sol +++ b/src/MulticallRootRouter.sol @@ -77,7 +77,7 @@ contract MulticallRootRouter is IRootRouter, Ownable { address public bridgeAgentExecutorAddress; /// @notice Re-entrancy lock modifier state. - uint256 internal _unlocked = 1; + uint8 internal _unlocked = 1;
_unlocked
with settlementNonce
(Save 1 SLOT: 2K gas)File:/src/RootBridgeAgent.sol#L74-L92 74: /// @notice Deposit nonce used for identifying transaction. 75: uint32 public settlementNonce; 78: mapping(uint256 nonce => Settlement settlementInfo) public getSettlement; 85: mapping(uint256 chainId => mapping(uint256 nonce => uint256 state)) public executionState; 92: uint256 internal _unlocked = 1;
Similar case with the previous instance, we can reduce the size of _unlocked
to uint8
and pack it with settlementNonce
since they are accessed on one transaction.
_unlocked
is being used by the modifier lock()
which is then applied to functions that access the variable settlementNonce
. see the function below
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L175-L195
File: /src/RootBridgeAgent.sol 175: function callOutAndBridge( 183: ) external payable override lock requiresRouter { 184: // Create Settlement and Perform call 185: bytes memory payload = _createSettlement( 186: settlementNonce, 195: );
We can see the above function uses the modifier lock
which has the following implementation. https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L1190-L1195
1190: modifier lock() { 1191: require(_unlocked == 1); 1192: _unlocked = 2; 1193: _; 1194: _unlocked = 1; 1195: }
We can save 1 SLOT by refactoring the code as follows
diff --git a/src/RootBridgeAgent.sol b/src/RootBridgeAgent.sol index a6ad0ef..8b5843d 100644 --- a/src/RootBridgeAgent.sol +++ b/src/RootBridgeAgent.sol @@ -67,29 +67,31 @@ contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants { /// @notice If true, bridge agent manager has allowed for a new given branch bridge agent to be synced/added. mapping(uint256 chainId => bool allowed) public isBranchBridgeAgentAllowed; + /*/////////////////////////////////////////////////////////////// - SETTLEMENTS STATE + EXECUTOR STATE //////////////////////////////////////////////////////////////*/ - /// @notice Deposit nonce used for identifying transaction. - uint32 public settlementNonce; + /// @notice If true, bridge agent has already served a request with this nonce from a given chain. Chain -> Nonce -> Bool + mapping(uint256 chainId => mapping(uint256 nonce => uint256 state)) public executionState; - /// @notice Mapping from Settlement nonce to Settlement Struct. - mapping(uint256 nonce => Settlement settlementInfo) public getSettlement; /*/////////////////////////////////////////////////////////////// - EXECUTOR STATE + SETTLEMENTS STATE //////////////////////////////////////////////////////////////*/ - /// @notice If true, bridge agent has already served a request with this nonce from a given chain. Chain -> Nonce -> Bool - mapping(uint256 chainId => mapping(uint256 nonce => uint256 state)) public executionState; + /// @notice Mapping from Settlement nonce to Settlement Struct. + mapping(uint256 nonce => Settlement settlementInfo) public getSettlement; + + /// @notice Deposit nonce used for identifying transaction. + uint32 public settlementNonce; /*/////////////////////////////////////////////////////////////// REENTRANCY STATE //////////////////////////////////////////////////////////////*/ /// @notice Re-entrancy lock modifier state. - uint256 internal _unlocked = 1; + uint8 internal _unlocked = 1;
_unlocked
with localBridgeAgentAddress
(Save 1 SLOT: 2K gas)File: /src/BaseBranchRouter.sol 32: /// @inheritdoc IBranchRouter 33: address public localPortAddress; 35: /// @inheritdoc IBranchRouter 36: address public override localBridgeAgentAddress; 38: /// @inheritdoc IBranchRouter 39: address public override bridgeAgentExecutorAddress; 41: /// @notice Re-entrancy lock modifier state. 42: uint256 internal _unlocked = 1;
Reduce the size of _unlocked
to uint8
and pack with localBridgeAgentAddress
. We are packing with localBridgeAgentAddress
because the variable _unlocked
and localBridgeAgentAddress
are being accessed in the same transaction
diff --git a/src/BaseBranchRouter.sol b/src/BaseBranchRouter.sol index 62bc287..275b77a 100644 --- a/src/BaseBranchRouter.sol +++ b/src/BaseBranchRouter.sol @@ -35,11 +35,13 @@ contract BaseBranchRouter is IBranchRouter, Ownable { /// @inheritdoc IBranchRouter address public override localBridgeAgentAddress; + /// @notice Re-entrancy lock modifier state. + uint8 internal _unlocked = 1; + /// @inheritdoc IBranchRouter address public override bridgeAgentExecutorAddress; - /// @notice Re-entrancy lock modifier state. - uint256 internal _unlocked = 1;
_unlocked
with depositNonce
(Save 1 SLOT: 2K gas)File: /src/BranchBridgeAgent.sol 84: uint32 public depositNonce; 87: mapping(uint256 depositNonce => Deposit depositInfo) public getDeposit; 94: mapping(uint256 settlementNonce => uint256 state) public executionState; 101: uint256 internal _unlocked = 1;
Reduce the size of _unlocked
to uint8
and pack with depositNonce
diff --git a/src/BranchBridgeAgent.sol b/src/BranchBridgeAgent.sol index b076d2d..0df2312 100644 --- a/src/BranchBridgeAgent.sol +++ b/src/BranchBridgeAgent.sol @@ -80,25 +80,27 @@ contract BranchBridgeAgent is IBranchBridgeAgent, BridgeAgentConstants { DEPOSITS STATE //////////////////////////////////////////////////////////////*/ - /// @notice Deposit nonce used for identifying the transaction. - uint32 public depositNonce; /// @notice Mapping from Pending deposits hash to Deposit Struct. mapping(uint256 depositNonce => Deposit depositInfo) public getDeposit; + + /// @notice Deposit nonce used for identifying the transaction. + uint32 public depositNonce; /*/////////////////////////////////////////////////////////////// - SETTLEMENT EXECUTION STATE + REENTRANCY STATE //////////////////////////////////////////////////////////////*/ - /// @notice If true, the bridge agent has already served a request with thi s nonce from a given chain. - mapping(uint256 settlementNonce => uint256 state) public executionState; + /// @notice Re-entrancy lock modifier state. + uint8 internal _unlocked = 1; /*/////////////////////////////////////////////////////////////// - REENTRANCY STATE + SETTLEMENT EXECUTION STATE //////////////////////////////////////////////////////////////*/ - /// @notice Re-entrancy lock modifier state. - uint256 internal _unlocked = 1; + /// @notice If true, the bridge agent has already served a request with this nonce from a given chain. + mapping(uint256 settlementNonce => uint256 state) public executionState; +
Starting at 1
(keeping the slot non-zero), will cost 5k per change (5k + 5k) vs 20k + 5k, saving you 15k gas
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L84C31-L84C32
File: /src/BranchBridgeAgent.sol 84: uint32 public depositNonce;
Since we are not initializing this value, the default is applied which is 0
. In the constructor , we initialize this value to 1
which means we move from 0 -> 1
We could save some gas by initializing this value when it's being declared. Also, since in the constructor we always assign a static value ie 1
we don't really need to have it there anymore if we initialize during declaration
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchBridgeAgent.sol#L117-L143
117: constructor( 124: ) { 140: depositNonce = 1; 143: }
Changing a value from 0 to 1 costs more than 20000 gas while changing from 1 to 2 would only cost 5000 gas
diff --git a/src/BranchBridgeAgent.sol b/src/BranchBridgeAgent.sol index b076d2d..6c9f60d 100644 --- a/src/BranchBridgeAgent.sol +++ b/src/BranchBridgeAgent.sol @@ -81,7 +81,7 @@ contract BranchBridgeAgent is IBranchBridgeAgent, BridgeAgentConstants { //////////////////////////////////////////////////////////////*/ /// @notice Deposit nonce used for identifying the transaction. - uint32 public depositNonce; + uint32 public depositNonce = 1; /// @notice Mapping from Pending deposits hash to Deposit Struct. mapping(uint256 depositNonce => Deposit depositInfo) public getDeposit; @@ -137,7 +137,6 @@ contract BranchBridgeAgent is IBranchBridgeAgent, BridgeAgentConstants { localRouterAddress = _localRouterAddress; localPortAddress = _localPortAddress; bridgeAgentExecutorAddress = DeployBranchBridgeAgentExecutor.deploy(); - depositNonce = 1; rootBridgeAgentPath = abi.encodePacked(_rootBridgeAgentAddress, address(this)); }
File: /src/RootBridgeAgent.sol 74: /// @notice Deposit nonce used for identifying transaction. 75: uint32 public settlementNonce;
The above variable is being assigned in the constructor as follows
121: settlementNonce = 1;
As with our previous case, since the variable was not assigned anything at declaration, it assumed the value 0
. This means when assigning the value in the constructor , we are basically moving from 0 -> 1
a typical case we see with bools
for true/false. Since the value to be assigned is hardcoded, there's no need to do this in the constructor, we can assign the value during declaration. This would eliminate the 0->1
which consumes too much gas.
diff --git a/src/RootBridgeAgent.sol b/src/RootBridgeAgent.sol index a6ad0ef..dcadf7c 100644 --- a/src/RootBridgeAgent.sol +++ b/src/RootBridgeAgent.sol @@ -72,7 +72,7 @@ contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants { //////////////////////////////////////////////////////////////*/ /// @notice Deposit nonce used for identifying transaction. - uint32 public settlementNonce; + uint32 public settlementNonce = 1; /// @notice Mapping from Settlement nonce to Settlement Struct. mapping(uint256 nonce => Settlement settlementInfo) public getSettlement; @@ -118,7 +118,6 @@ contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants { localPortAddress = _localPortAddress; localRouterAddress = _localRouterAddress; bridgeAgentExecutorAddress = DeployRootBridgeAgentExecutor.deploy(address(this)); - settlementNonce = 1; }
redeemDeposit
by refactoring the code to fail Early - 2.1k half of the time(scenario where sender is address(0))File: /src/BranchBridgeAgent.sol 434: function redeemDeposit(uint32 _depositNonce) external override lock { 435: // Get storage reference 436: Deposit storage deposit = getDeposit[_depositNonce]; 438: // Check Deposit 439: if (deposit.status == STATUS_SUCCESS) revert DepositRedeemUnavailable(); 440: if (deposit.owner == address(0)) revert DepositRedeemUnavailable(); 441: if (deposit.owner != msg.sender) revert NotDepositOwner();
Of interest to us is the last two if statements, we first verify that deposit.owner
is not equal to address(0)
and revert if it happens to be address(0)
We then validate that the msg.sender
is in fact the deposit.owner
, so for the last check to pass, we would need the deposit.owner == msg.sender
to hold
If we reorder the two checks and validate deposit.owner != msg.sender
first, it would mean any occurrence of the variable deposit.owner
can be replaced by msg.sender
since msg.sender
would be equal to deposit.owner
// Check Deposit if (deposit.status == STATUS_SUCCESS) revert DepositRedeemUnavailable(); - if (deposit.owner == address(0)) revert DepositRedeemUnavailable(); if (deposit.owner != msg.sender) revert NotDepositOwner(); + if (msg.sender == address(0)) revert DepositRedeemUnavailable();
After the first refactor, we get a clear view of how the checks are being done and we can now refactor even more by moving the check msg.sender == address()
to the top before reading any state variables
We can therefore save an SLOAD (Gscold = 2.1K) gas and another two sloads for reading the variable deposit.status
and deposit.owner
function redeemDeposit(uint32 _depositNonce) external override lock { + if (msg.sender == address(0)) revert DepositRedeemUnavailable(); // Get storage reference Deposit storage deposit = getDeposit[_depositNonce]; // Check Deposit if (deposit.status == STATUS_SUCCESS) revert DepositRedeemUnavailable(); - if (deposit.owner == address(0)) revert DepositRedeemUnavailable(); if (deposit.owner != msg.sender) revert NotDepositOwner();
settlementReference.owner
in memoryFile: /src/RootBridgeAgent.sol 233: function retrySettlement( 239: ) external payable override lock { 240: // Get storage reference 241: Settlement storage settlementReference = getSettlement[_settlementNonce]; 244: if (settlementReference.owner == address(0)) revert SettlementRetryUnavailable(); 247: if (msg.sender != settlementReference.owner) { 248: if (msg.sender != address(IPort(localPortAddress).getUserAccount(settlementReference.owner))) { 249: revert NotSettlementOwner(); 250: } 251: } 257: _performRetrySettlementCall( 265: payable(settlementReference.owner), 270: ); 271: }
The following suggestion is already being applied in other parts of this code, see https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L279
diff --git a/src/RootBridgeAgent.sol b/src/RootBridgeAgent.sol index a6ad0ef..a1fd294 100644 --- a/src/RootBridgeAgent.sol +++ b/src/RootBridgeAgent.sol @@ -240,12 +240,15 @@ contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants { // Get storage reference Settlement storage settlementReference = getSettlement[_settlementNonce]; + // Get Settlement owner. + address settlementOwner = settlementReference.owner; + // Check if Settlement hasn't been redeemed. - if (settlementReference.owner == address(0)) revert SettlementRetryUnavailable(); + if (settlementOwner == address(0)) revert SettlementRetryUnavailable(); // Check if caller is Settlement owner - if (msg.sender != settlementReference.owner) { - if (msg.sender != address(IPort(localPortAddress).getUserAccount(settlementReference.owner))) { + if (msg.sender != settlementOwner) { + if (msg.sender != address(IPort(localPortAddress).getUserAccount(settlementOwner))) { revert NotSettlementOwner(); } } @@ -262,7 +265,7 @@ contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants { settlementReference.deposits, _params, _settlementNonce, - payable(settlementReference.owner), + payable(settlementOwner), _recipient, settlementReference.dstChainId, _gParams,
settlementReference.owner
in memoryFile: /src/RootBridgeAgent.sol 667: Settlement storage settlementReference = getSettlement[nonce]; 670: if (settlementReference.owner == address(0)) revert SettlementRetryUnavailable(); 673: if (owner != settlementReference.owner) { 674: if (owner != address(IPort(localPortAddress).getUserAccount(settlementReference.owner))) { 675: revert NotSettlementOwner(); 676: } 677: } 683: _performRetrySettlementCall( 691: payable(settlementReference.owner),
Settlement storage settlementReference = getSettlement[nonce]; + address settlementOwner = settlementReference.owner; + // Check if Settlement hasn't been redeemed. - if (settlementReference.owner == address(0)) revert SettlementRetryUnavailable(); + if (settlementOwner == address(0)) revert SettlementRetryUnavailable(); // Check settlement owner - if (owner != settlementReference.owner) { - if (owner != address(IPort(localPortAddress).getUserAccount(settlementReference.owner))) { + if (owner != settlementOwner) { + if (owner != address(IPort(localPortAddress).getUserAccount(settlementOwner))) { revert NotSettlementOwner(); } } @@ -688,7 +690,7 @@ contract RootBridgeAgent is IRootBridgeAgent, BridgeAgentConstants { settlementReference.deposits, params, nonce, - payable(settlementReference.owner), + payable(settlementOwner),
File: /src/factories/ERC20hTokenRootFactory.sol 96: modifier requiresCoreRouterOrPort() { 97: if (msg.sender != coreRootRouterAddress) { 98: if (msg.sender != rootPortAddress) { 99: revert UnrecognizedCoreRouterOrPort(); 100: } 101: } 102: _; 103: }
The above modifier is only used on https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/factories/ERC20hTokenRootFactory.sol#L78
Similar instances https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchPort.sol#L553-L556
File: /src/BranchPort.sol 553: modifier requiresBridgeAgentFactory() { 554: if (!isBridgeAgentFactory[msg.sender]) revert UnrecognizedBridgeAgentFactory(); 555: _; 556: }
The above modifier is only used on https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchPort.sol#L319
File: /src/BranchPort.sol 559: modifier requiresPortStrategy(address _token) { 560: if (!isStrategyToken[_token]) revert UnrecognizedStrategyToken(); 561: if (!isPortStrategy[msg.sender][_token]) revert UnrecognizedPortStrategy(); 562: _; 563: }
The modifier above is only called on https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/BranchPort.sol#L144
File: /src/ArbitrumBranchPort.sol 57: address _rootPortAddress = rootPortAddress; 60: address _globalToken = IRootPort(_rootPortAddress).getLocalTokenFromUnderlying(_underlyingAddress, localChainId); 63: if (_globalToken == address(0)) revert UnknownGlobalToken(); 66: _underlyingAddress.safeTransferFrom(_depositor, address(this), _deposit); 69: IRootPort(_rootPortAddress).mintToLocalBranch(_recipient, _globalToken, _deposit);
diff --git a/src/ArbitrumBranchPort.sol b/src/ArbitrumBranchPort.sol index 5777422..cc9c872 100644 --- a/src/ArbitrumBranchPort.sol +++ b/src/ArbitrumBranchPort.sol @@ -53,11 +53,8 @@ contract ArbitrumBranchPort is BranchPort, IArbitrumBranchPort { lock requiresBridgeAgent { - // Save root port address to memory - address _rootPortAddress = rootPortAddress; - // Get global token address from root port - address _globalToken = IRootPort(_rootPortAddress).getLocalTokenFromUnderlying(_underlyingAddress, localChainId); + address _globalToken = IRootPort(rootPortAddress).getLocalTokenFromUnderlying(_underlyingAddress, localChainId); // Check if the global token exists if (_globalToken == address(0)) revert UnknownGlobalToken(); @@ -66,7 +63,7 @@ contract ArbitrumBranchPort is BranchPort, IArbitrumBranchPort { _underlyingAddress.safeTransferFrom(_depositor, address(this), _deposit); // Request Minting of Global Token - IRootPort(_rootPortAddress).mintToLocalBranch(_recipient, _globalToken, _deposit); + IRootPort(rootPortAddress).mintToLocalBranch(_recipient, _globalToken, _deposit); }
File: /src/ArbitrumBranchPort.sol 80: address _rootPortAddress = rootPortAddress; 83: if (!IRootPort(_rootPortAddress).isGlobalToken(_globalAddress, localChainId)) revert UnknownGlobalToken(); 86: address _underlyingAddress = 87: IRootPort(_rootPortAddress).getUnderlyingTokenFromLocal(_globalAddress, localChainId); 90: if (_underlyingAddress == address(0)) revert UnknownUnderlyingToken(); 92: IRootPort(_rootPortAddress).burnFromLocalBranch(_depositor, _globalAddress, _amount);
diff --git a/src/ArbitrumBranchPort.sol b/src/ArbitrumBranchPort.sol index 5777422..3124d7d 100644 --- a/src/ArbitrumBranchPort.sol +++ b/src/ArbitrumBranchPort.sol @@ -76,20 +76,18 @@ contract ArbitrumBranchPort is BranchPort, IArbitrumBranchPort { lock requiresBridgeAgent { - // Save root port address to memory - address _rootPortAddress = rootPortAddress; // Check if the global token exists - if (!IRootPort(_rootPortAddress).isGlobalToken(_globalAddress, localChainId)) revert UnknownGlobalToken(); + if (!IRootPort(rootPortAddress).isGlobalToken(_globalAddress, localChainId)) revert UnknownGlobalToken(); // Get the underlying token address from the root port address _underlyingAddress = - IRootPort(_rootPortAddress).getUnderlyingTokenFromLocal(_globalAddress, localChainId); + IRootPort(rootPortAddress).getUnderlyingTokenFromLocal(_globalAddress, localChainId); // Check if the underlying token exists if (_underlyingAddress == address(0)) revert UnknownUnderlyingToken(); - IRootPort(_rootPortAddress).burnFromLocalBranch(_depositor, _globalAddress, _amount); + IRootPort(rootPortAddress).burnFromLocalBranch(_depositor, _globalAddress, _amount); _underlyingAddress.safeTransfer(_recipient, _amount); }
It is important to emphasize that the provided recommendations aim to enhance the efficiency of the code without compromising its readability. We understand the value of maintainable and easily understandable code to both developers and auditors.
As you proceed with implementing the suggested optimizations, please exercise caution and be diligent in conducting thorough testing. It is crucial to ensure that the changes are not introducing any new vulnerabilities and that the desired performance improvements are achieved. Review code changes, and perform thorough testing to validate the effectiveness and security of the refactored code.
Should you have any questions or need further assistance, please don't hesitate to reach out.
#0 - c4-pre-sort
2023-10-15T16:59:36Z
0xA5DF marked the issue as sufficient quality report
#1 - c4-judge
2023-10-26T13:48:30Z
alcueca marked the issue as grade-a