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: 138/175
Findings: 1
Award: $11.47
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: MrPotatoMagic
Also found by: 0xAadi, 0xDING99YA, 0xDemon, 0xRstStn, 0xSmartContract, 0xStriker, 0xWaitress, 0xbrett8571, 0xfuje, 0xsagetony, 0xsurena, 33BYTEZZZ, 3docSec, 7ashraf, ABA, ABAIKUNANBAEV, Aamir, Audinarey, Bauchibred, Black_Box_DD, Daniel526, DanielArmstrong, DanielTan_MetaTrust, Dinesh11G, Eurovickk, Franklin, Inspecktor, John, Jorgect, Joshuajee, K42, Kek, Koolex, LokiThe5th, MIQUINHO, Myd, NoTechBG, QiuhaoLi, SanketKogekar, Sathish9098, Sentry, Soul22, SovaSlava, Stormreckson, Tendency, Topmark, Udsen, V1235816, Viktor_Cortess, Viraz, Yanchuan, ZdravkoHr, Zims, albahaca, albertwh1te, alexweb3, alexxander, ast3ros, audityourcontracts, bareli, bin2chen, bronze_pickaxe, c0pp3rscr3w3r, cartlex_, castle_chain, chaduke, debo, ether_sky, gumgumzum, imare, its_basu, jaraxxus, jasonxiale, josephdara, kodyvim, ladboy233, lanrebayode77, lsaudit, mert_eren, minhtrng, n1punp, nadin, niroh, nmirchev8, orion, peakbolt, perseverancesuccess, pfapostol, ptsanev, rvierdiiev, saneryee, shaflow2, te_aut, terrancrypt, twcctop, unsafesol, ustas, versiyonbir, windhustler, yongskiws, zhaojie, ziyou-
11.4657 USDC - $11.47
https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/MulticallRootRouter.sol#L203-L205 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/interfaces/BridgeAgentConstants.sol#L15 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/CoreRootRouter.sol#L350-L357 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgentExecutor.sol#L82-L106 https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/RootBridgeAgent.sol#L490-L512
The callOutAndBridge function on Branches allow users to bridge tokens to other chains, which first need to go through the root implementation on Arbitrum. However, there is a chance after bridging to the root, funds get stuck on the RootCoreRouter contract with the deposit nonce being spent when no extra data is passed into the payload.
//SPDX-License-Identifier: MIT pragma solidity 0.8.19; import "./ulysses-omnichain/helpers/ImportHelper.sol"; contract TestPocTest is DSTestPlus, BridgeAgentConstants { // Consts uint16 constant rootChainId = uint16(42161); uint16 constant avaxChainId = uint16(43114); uint16 constant ftmChainId = uint16(2040); // Root RootPort rootPort; ERC20hTokenRootFactory hTokenFactory; RootBridgeAgentFactory bridgeAgentFactory; RootBridgeAgent coreRootBridgeAgent; RootBridgeAgent multicallBridgeAgent; CoreRootRouter rootCoreRouter; MulticallRootRouter rootMulticallRouter; address lzEndpointAddress = address(new MockEndpoint()); address owner = address(this); address dao = address(this); // Arbitrum Branch ArbitrumBranchPort arbitrumPort; ERC20hTokenBranchFactory localHTokenFactory; ArbitrumBranchBridgeAgentFactory arbitrumBranchBridgeAgentFactory; ArbitrumBranchBridgeAgent arbitrumCoreBridgeAgent; ArbitrumBranchBridgeAgent arbitrumMulticallBridgeAgent; ArbitrumCoreBranchRouter arbitrumCoreRouter; BaseBranchRouter arbitrumMulticallRouter; // Avax Branch BranchPort avaxPort; ERC20hTokenBranchFactory avaxHTokenFactory; BranchBridgeAgentFactory avaxBranchBridgeAgentFactory; BranchBridgeAgent avaxCoreBridgeAgent; BranchBridgeAgent avaxMulticallBridgeAgent; CoreBranchRouter avaxCoreRouter; BaseBranchRouter avaxMulticallRouter; // Ftm Branch BranchPort ftmPort; ERC20hTokenBranchFactory ftmHTokenFactory; BranchBridgeAgentFactory ftmBranchBridgeAgentFactory; BranchBridgeAgent ftmCoreBridgeAgent; BranchBridgeAgent ftmMulticallBridgeAgent; CoreBranchRouter ftmCoreRouter; BaseBranchRouter ftmMulticallRouter; // Mocks address arbitrumGlobalToken; address avaxGlobalToken; address ftmGlobalToken; address arbitrumWrappedNativeToken; address avaxWrappedNativeToken; address ftmWrappedNativeToken; address arbitrumLocalWrappedNativeToken; address avaxLocalWrappedNativeToken; address ftmLocalWrappedNativeToken; address multicallAddress; address testGasPoolAddress = address(0xFFFF); address nonFungiblePositionManagerAddress = address(0xABAD); address avaxLocalarbitrumWrappedNativeTokenAddress = address(0xBFFF); address avaxUnderlyingarbitrumWrappedNativeTokenAddress = address(0xFFFB); address ftmLocalarbitrumWrappedNativeTokenAddress = address(0xABBB); address ftmUnderlyingarbitrumWrappedNativeTokenAddress = address(0xAAAB); address avaxCoreBridgeAgentAddress = address(0xBEEF); address avaxMulticallBridgeAgentAddress = address(0xEBFE); address avaxPortAddress = address(0xFEEB); address ftmCoreBridgeAgentAddress = address(0xCACA); address ftmMulticallBridgeAgentAddress = address(0xACAC); address ftmPortAddressM = address(0xABAC); address public newAvaxAssetLocalTokenAddress; address public newFTMAvaxAssetLocalToken; address public newAvaxAssetGlobalAddress; // ERC20s from different chains. address avaxMockAssethToken; MockERC20 avaxMockAssetToken; address ftmMockAssethToken; MockERC20 ftmMockAssetToken; ERC20hTokenRoot arbitrumMockAssethToken; MockERC20 arbitrumMockToken; function setUp() public { ///////////////////////////////// // Deploy Root Utils // ///////////////////////////////// arbitrumWrappedNativeToken = address(new WETH()); avaxWrappedNativeToken = address(new WETH()); ftmWrappedNativeToken = address(new WETH()); multicallAddress = address(new Multicall2()); ///////////////////////////////// // Deploy Root Contracts // ///////////////////////////////// rootPort = new RootPort(rootChainId); bridgeAgentFactory = new RootBridgeAgentFactory( rootChainId, lzEndpointAddress, address(rootPort) ); rootCoreRouter = new CoreRootRouter(rootChainId, address(rootPort)); rootMulticallRouter = new MulticallRootRouter( rootChainId, address(rootPort), multicallAddress ); hTokenFactory = new ERC20hTokenRootFactory( rootChainId, address(rootPort) ); ///////////////////////////////// // Initialize Root Contracts // ///////////////////////////////// rootPort.initialize( address(bridgeAgentFactory), address(rootCoreRouter) ); hevm.deal(address(rootPort), 1 ether); hevm.prank(address(rootPort)); WETH(arbitrumWrappedNativeToken).deposit{value: 1 ether}(); hTokenFactory.initialize(address(rootCoreRouter)); coreRootBridgeAgent = RootBridgeAgent( payable( RootBridgeAgentFactory(bridgeAgentFactory).createBridgeAgent( address(rootCoreRouter) ) ) ); multicallBridgeAgent = RootBridgeAgent( payable( RootBridgeAgentFactory(bridgeAgentFactory).createBridgeAgent( address(rootMulticallRouter) ) ) ); rootCoreRouter.initialize( address(coreRootBridgeAgent), address(hTokenFactory) ); rootMulticallRouter.initialize(address(multicallBridgeAgent)); ///////////////////////////////// // Deploy Local Branch Contracts// ///////////////////////////////// arbitrumPort = new ArbitrumBranchPort( rootChainId, address(rootPort), owner ); arbitrumMulticallRouter = new BaseBranchRouter(); arbitrumCoreRouter = new ArbitrumCoreBranchRouter(); arbitrumBranchBridgeAgentFactory = new ArbitrumBranchBridgeAgentFactory( rootChainId, address(bridgeAgentFactory), address(arbitrumCoreRouter), address(arbitrumPort), owner ); arbitrumPort.initialize( address(arbitrumCoreRouter), address(arbitrumBranchBridgeAgentFactory) ); arbitrumBranchBridgeAgentFactory.initialize( address(coreRootBridgeAgent) ); arbitrumCoreBridgeAgent = ArbitrumBranchBridgeAgent( payable(arbitrumPort.bridgeAgents(0)) ); arbitrumCoreRouter.initialize(address(arbitrumCoreBridgeAgent)); // ArbitrumMulticallRouter.initialize(address(arbitrumMulticallBridgeAgent)); ////////////////////////////////// // Deploy Avax Branch Contracts // ////////////////////////////////// avaxPort = new BranchPort(owner); avaxHTokenFactory = new ERC20hTokenBranchFactory( rootChainId, address(avaxPort), "Avalanche Ulysses ", "avax-u" ); avaxMulticallRouter = new BaseBranchRouter(); avaxCoreRouter = new CoreBranchRouter(address(avaxHTokenFactory)); avaxBranchBridgeAgentFactory = new BranchBridgeAgentFactory( avaxChainId, rootChainId, address(bridgeAgentFactory), lzEndpointAddress, address(avaxCoreRouter), address(avaxPort), owner ); avaxPort.initialize( address(avaxCoreRouter), address(avaxBranchBridgeAgentFactory) ); avaxBranchBridgeAgentFactory.initialize(address(coreRootBridgeAgent)); avaxCoreBridgeAgent = BranchBridgeAgent( payable(avaxPort.bridgeAgents(0)) ); avaxHTokenFactory.initialize( avaxWrappedNativeToken, address(avaxCoreRouter) ); avaxLocalWrappedNativeToken = 0x386Cc0A3450d41747C05C62381320C039C65ee0d; avaxCoreRouter.initialize(address(avaxCoreBridgeAgent)); ////////////////////////////////// // Deploy Ftm Branch Contracts // ////////////////////////////////// ftmPort = new BranchPort(owner); ftmHTokenFactory = new ERC20hTokenBranchFactory( rootChainId, address(ftmPort), "Fantom Ulysses ", "ftm-u" ); ftmMulticallRouter = new BaseBranchRouter(); ftmCoreRouter = new CoreBranchRouter(address(ftmHTokenFactory)); ftmBranchBridgeAgentFactory = new BranchBridgeAgentFactory( ftmChainId, rootChainId, address(bridgeAgentFactory), lzEndpointAddress, address(ftmCoreRouter), address(ftmPort), owner ); ftmPort.initialize( address(ftmCoreRouter), address(ftmBranchBridgeAgentFactory) ); ftmBranchBridgeAgentFactory.initialize(address(coreRootBridgeAgent)); ftmCoreBridgeAgent = BranchBridgeAgent( payable(ftmPort.bridgeAgents(0)) ); ftmHTokenFactory.initialize( ftmWrappedNativeToken, address(ftmCoreRouter) ); ftmLocalWrappedNativeToken = 0x0315E8648695243BCE3Da6a0Ce973867B75Db847; ftmCoreRouter.initialize(address(ftmCoreBridgeAgent)); ///////////////////////////// // Add new branch chains // ///////////////////////////// RootPort(rootPort).addNewChain( address(avaxCoreBridgeAgent), avaxChainId, "Avalanche", "AVAX", 18, avaxLocalWrappedNativeToken, avaxWrappedNativeToken ); RootPort(rootPort).addNewChain( address(ftmCoreBridgeAgent), ftmChainId, "Fantom Opera", "FTM", 18, ftmLocalWrappedNativeToken, ftmWrappedNativeToken ); avaxGlobalToken = RootPort(rootPort).getGlobalTokenFromLocal( avaxLocalWrappedNativeToken, avaxChainId ); ftmGlobalToken = RootPort(rootPort).getGlobalTokenFromLocal( ftmLocalWrappedNativeToken, ftmChainId ); ////////////////////// // Verify Addition // ////////////////////// require( RootPort(rootPort).isGlobalAddress(avaxGlobalToken), "Token should be added" ); require( RootPort(rootPort).getGlobalTokenFromLocal( address(avaxLocalWrappedNativeToken), avaxChainId ) == avaxGlobalToken, "Token should be added" ); require( RootPort(rootPort).getLocalTokenFromGlobal( avaxGlobalToken, avaxChainId ) == address(avaxLocalWrappedNativeToken), "Token should be added" ); require( RootPort(rootPort).getUnderlyingTokenFromLocal( address(avaxLocalWrappedNativeToken), avaxChainId ) == address(avaxWrappedNativeToken), "Token should be added" ); require( RootPort(rootPort).getGlobalTokenFromLocal( address(ftmLocalWrappedNativeToken), ftmChainId ) == ftmGlobalToken, "Token should be added" ); require( RootPort(rootPort).getLocalTokenFromGlobal( ftmGlobalToken, ftmChainId ) == address(ftmLocalWrappedNativeToken), "Token should be added" ); require( RootPort(rootPort).getUnderlyingTokenFromLocal( address(ftmLocalWrappedNativeToken), ftmChainId ) == address(ftmWrappedNativeToken), "Token should be added" ); /////////////////////////////////// // Approve new Branchs in Root // /////////////////////////////////// rootPort.initializeCore( address(coreRootBridgeAgent), address(arbitrumCoreBridgeAgent), address(arbitrumPort) ); multicallBridgeAgent.approveBranchBridgeAgent(rootChainId); multicallBridgeAgent.approveBranchBridgeAgent(avaxChainId); multicallBridgeAgent.approveBranchBridgeAgent(ftmChainId); /////////////////////////////////////// // Add new branches to Root Agents // /////////////////////////////////////// hevm.deal(address(this), 3 ether); rootCoreRouter.addBranchToBridgeAgent{value: 1 ether}( address(multicallBridgeAgent), address(avaxBranchBridgeAgentFactory), address(avaxCoreRouter), address(this), avaxChainId, [GasParams(0.05 ether, 0.05 ether), GasParams(0.02 ether, 0)] ); rootCoreRouter.addBranchToBridgeAgent{value: 1 ether}( address(multicallBridgeAgent), address(ftmBranchBridgeAgentFactory), address(ftmCoreRouter), address(this), ftmChainId, [GasParams(0.05 ether, 0.05 ether), GasParams(0.02 ether, 0)] ); rootCoreRouter.addBranchToBridgeAgent( address(multicallBridgeAgent), address(arbitrumBranchBridgeAgentFactory), address(arbitrumCoreRouter), address(this), rootChainId, [GasParams(0, 0), GasParams(0, 0)] ); ///////////////////////////////////// // Initialize new Branch Routers // ///////////////////////////////////// arbitrumMulticallBridgeAgent = ArbitrumBranchBridgeAgent( payable(arbitrumPort.bridgeAgents(1)) ); avaxMulticallBridgeAgent = BranchBridgeAgent( payable(avaxPort.bridgeAgents(1)) ); ftmMulticallBridgeAgent = BranchBridgeAgent( payable(ftmPort.bridgeAgents(1)) ); arbitrumMulticallRouter.initialize( address(arbitrumMulticallBridgeAgent) ); avaxMulticallRouter.initialize(address(avaxMulticallBridgeAgent)); ftmMulticallRouter.initialize(address(ftmMulticallBridgeAgent)); ////////////////////////////////////// // Deploy Underlying Tokens and Mocks// ////////////////////////////////////// avaxMockAssetToken = new MockERC20("underlying token", "UNDER", 18); ftmMockAssetToken = new MockERC20("underlying token", "UNDER", 18); ///on Avax, add local token. hevm.deal(address(this), 1 ether); avaxCoreRouter.addLocalToken{value: 0.1 ether}( address(avaxMockAssetToken), GasParams(0.5 ether, 0.5 ether) ); newAvaxAssetLocalTokenAddress = RootPort(rootPort) .getLocalTokenFromUnderlying( 0x541dC483Eb43cf8F9969baF71BF783193e5C5B1A, avaxChainId ); console2.log("New: ", newAvaxAssetLocalTokenAddress); newAvaxAssetGlobalAddress = RootPort(rootPort).getGlobalTokenFromLocal( address(newAvaxAssetLocalTokenAddress), avaxChainId ); GasParams[3] memory gasParams = [ GasParams(0.05 ether, 0.05 ether), GasParams(0.05 ether, 0.0025 ether), GasParams(0.002 ether, 0) ]; avaxCoreRouter.addGlobalToken{value: 0.15 ether}( newAvaxAssetGlobalAddress, ftmChainId, gasParams ); newFTMAvaxAssetLocalToken = RootPort(rootPort).getLocalTokenFromGlobal( newAvaxAssetGlobalAddress, ftmChainId ); console2.log("New Local: ", newFTMAvaxAssetLocalToken); require( RootPort(rootPort).getLocalTokenFromGlobal( newAvaxAssetGlobalAddress, ftmChainId ) == newFTMAvaxAssetLocalToken, "Token should be added" ); require( RootPort(rootPort).getUnderlyingTokenFromLocal( newFTMAvaxAssetLocalToken, ftmChainId ) == address(0), "Underlying should not be added" ); } function testPOC_callOutAndBridgeStuckFunds() public { ///@dev mocking call from the perspective of lzReceive on root chain. address hToken = newAvaxAssetLocalTokenAddress; address underlyingToken = address(avaxMockAssetToken); address alice = hevm.addr(0xA11cE); ///@notice extra data not passed into payload bytes memory payload = abi.encodePacked( bytes1(0x02), uint32(99), hToken, underlyingToken, uint256(100e18), uint256(100e18), "" ); console2.log( "lz endpoint in Agent: ", coreRootBridgeAgent.lzEndpointAddress() ); console2.log("lz endpoint in test: ", lzEndpointAddress); uint256 coreRouterBalancePrior = MockERC20(newAvaxAssetGlobalAddress) .balanceOf(address(rootCoreRouter)); uint256 userBalancePrior = MockERC20(newAvaxAssetGlobalAddress) .balanceOf(alice); console2.log("Alice's balance prior: ", userBalancePrior); console2.log("coreRootRouterBalance prior: ", coreRouterBalancePrior); hevm.startPrank(lzEndpointAddress); coreRootBridgeAgent.lzReceive{gas: 0.5 ether}( 43114, abi.encodePacked( payable(coreRootBridgeAgent), payable(avaxCoreBridgeAgent) ), 1, payload ); hevm.stopPrank(); ///@dev uint8 internal constant STATUS_DONE = 1; ///@notice reference in: https://github.com/code-423n4/2023-09-maia/blob/f5ba4de628836b2a29f9b5fff59499690008c463/src/interfaces/BridgeAgentConstants.sol#L15 assertEq( coreRootBridgeAgent.executionState(43114, 99), 1, "Nonce not spent?" ); uint256 coreRouterBalanceAfter = MockERC20(newAvaxAssetGlobalAddress) .balanceOf(address(rootCoreRouter)); uint256 userBalanceAfter = MockERC20(newAvaxAssetGlobalAddress) .balanceOf(alice); console2.log("Alice's balance prior: ", userBalanceAfter); console2.log("coreRootRouterBalance prior: ", coreRouterBalanceAfter); assertEq( userBalanceAfter, userBalancePrior, "Alice's balance did not change on the root chain" ); assertEq( coreRouterBalanceAfter, coreRouterBalancePrior + 100e18, "The router balance changes by 100e18 tokens" ); } } contract MockEndpoint is DSTestPlus { uint256 constant rootChain = 42161; address public sourceBridgeAgent; address public destinationBridgeAgent; bytes public data; uint32 public nonce; bool forceFallback; uint256 fallbackCountdown; uint256 gasLimit; uint256 remoteBranchExecutionGas; address receiver; constructor() {} function toggleFallback(uint256 _fallbackCountdown) external { forceFallback = !forceFallback; fallbackCountdown = _fallbackCountdown; } function sendFallback() public { console2.log("Mocking fallback..."); console2.log("sourceBridgeAgent:", sourceBridgeAgent); console2.log("destinationBridgeAgent:", destinationBridgeAgent); console2.log( "srcChainId:", BranchBridgeAgent(payable(sourceBridgeAgent)).localChainId() ); hevm.deal( address(this), (gasLimit + remoteBranchExecutionGas) * tx.gasprice ); bytes memory fallbackData = abi.encodePacked( BranchBridgeAgent(payable(sourceBridgeAgent)).localChainId() == rootChain ? 0x09 : 0x04, nonce ); // Perform Call sourceBridgeAgent.call{value: remoteBranchExecutionGas}(""); RootBridgeAgent(payable(sourceBridgeAgent)).lzReceive{gas: gasLimit}( BranchBridgeAgent(payable(destinationBridgeAgent)).localChainId(), abi.encodePacked(sourceBridgeAgent, destinationBridgeAgent), 1, fallbackData ); } // @notice send a LayerZero message to the specified address at a LayerZero endpoint. // @param _dstChainId - the destination chain identifier // @param _destination - the address on destination chain (in bytes). address length/format may vary by chains // @param _payload - a custom bytes payload to send to the destination contract // @param _refundAddress - if the source transaction is cheaper than the amount of value passed, refund the additional amount to this address // @param _zroPaymentAddress - the address of the ZRO token holder who would pay for the transaction // @param _adapterParams - parameters for custom functionality. e.g. receive airdropped native gas from the relayer on destination function send( uint16 _dstChainId, bytes calldata _destination, bytes calldata _payload, address payable, address, bytes calldata _adapterParams ) external payable { sourceBridgeAgent = msg.sender; destinationBridgeAgent = address(bytes20(_destination[:20])); data = _payload; nonce = _dstChainId == uint16(42161) ? BranchBridgeAgent(payable(msg.sender)).depositNonce() - 1 : RootBridgeAgent(payable(msg.sender)).settlementNonce() - 1; console2.log("Mocking lzSends..."); console2.log("sourceBridgeAgent:", msg.sender); console2.log("destinationBridgeAgent:", destinationBridgeAgent); console2.log( "srcChainId:", BranchBridgeAgent(payable(msg.sender)).localChainId() ); // Decode adapter params if (_adapterParams.length > 0) { gasLimit = uint256(bytes32(_adapterParams[0:32])); remoteBranchExecutionGas = uint256(bytes32(_adapterParams[32:64])); receiver = address(bytes20(_adapterParams[64:84])); } else { gasLimit = 200_000; remoteBranchExecutionGas = 0; receiver = address(0); } if (!forceFallback) { // Perform Call destinationBridgeAgent.call{value: remoteBranchExecutionGas}(""); RootBridgeAgent(payable(destinationBridgeAgent)).lzReceive{ gas: gasLimit }( BranchBridgeAgent(payable(msg.sender)).localChainId(), _destination, 1, data ); } else if (fallbackCountdown > 0) { console2.log("Execute LayerZero request...", fallbackCountdown--); // Perform Call destinationBridgeAgent.call{value: remoteBranchExecutionGas}(""); RootBridgeAgent(payable(destinationBridgeAgent)).lzReceive{ gas: gasLimit }( BranchBridgeAgent(payable(msg.sender)).localChainId(), _destination, 1, data ); } } }
Foundry
In the case where no extra data is passed into the payload to be used on the root chain to forward the tokens to the intended destination, the transaction should be reverted, allowing the user to retrieve their deposit on the branch chain they're bridging from.
Token-Transfer
#0 - c4-pre-sort
2023-10-14T12:45:17Z
0xA5DF marked the issue as duplicate of #898
#1 - c4-pre-sort
2023-10-14T12:45:22Z
0xA5DF marked the issue as sufficient quality report
#2 - c4-judge
2023-10-25T12:41:51Z
alcueca marked the issue as duplicate of #685
#3 - c4-judge
2023-10-25T13:12:56Z
alcueca changed the severity to QA (Quality Assurance)
#4 - alcueca
2023-10-25T13:30:40Z
The Router is not expected to hold funds, and callers of unsigned functions should know that. They are minted in the Router to be immediately used. If they make an error and leave their tokens in the Router, then it not expected that they will be protected.
#5 - c4-judge
2023-10-27T10:16:19Z
alcueca marked the issue as grade-b