Platform: Code4rena
Start Date: 30/04/2024
Pot Size: $112,500 USDC
Total HM: 22
Participants: 122
Period: 8 days
Judge: alcueca
Total Solo HM: 1
Id: 372
League: ETH
Rank: 51/122
Findings: 6
Award: $17.75
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: LessDupes
Also found by: 0rpse, 0xAadi, 0xCiphky, 0xhacksmithh, 0xnightfall, FastChecker, KupiaSec, NentoR, SBSecurity, Tendency, adam-idarrha, aman, araj, baz1ka, bigtone, fyamf, jokr, kennedy1030, maxim371, mussucal, p0wd3r, zigtur
13.5262 USDC - $13.53
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Delegation/OperatorDelegator.sol#L327-L335 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L274-L358
Incorrect token address specified in OperatorDelegator.sol#getTokenBalanceFromStrategy()
function
As a result, TVL is calculated incorrectly and the ezETH token can be mint or withdraw at invalid prices.
The OperatorDelegator.sol#getTokenBalanceFromStrategy()
function used to get the underlying token amount from the amount of shares + queued withdrawal shares of the OperatorDelegator in the RestakeManager.sol#calculateTVLs()
function.
function calculateTVLs() public view returns (uint256[][] memory, uint256[] memory, uint256) { SNIP... for (uint256 i = 0; i < odLength; ) { // Track the TVL for this OD uint256 operatorTVL = 0; // Track the individual token TVLs for this OD - native ETH will be last item in the array uint256[] memory operatorValues = new uint256[](collateralTokens.length + 1); operatorDelegatorTokenTVLs[i] = operatorValues; // Iterate through the tokens and get the value of each uint256 tokenLength = collateralTokens.length; for (uint256 j = 0; j < tokenLength; ) { // Get the value of this token 302: uint256 operatorBalance = operatorDelegators[i].getTokenBalanceFromStrategy( 303: collateralTokens[j] 304: ); // Set the value in the array for this OD 307: operatorValues[j] = renzoOracle.lookupTokenValue( collateralTokens[j], operatorBalance ); // Add it to the total TVL for this OD 313: operatorTVL += operatorValues[j]; SNIP... } SNIP... // Add it to the total TVL for the protocol 338: totalTVL += operatorTVL; SNIP... } SNIP... return (operatorDelegatorTokenTVLs, operatorDelegatorTVLs, totalTVL); }
As you can see, the underlying token amount from the amount of shares + pending withdrawal shares of the operatorDelegator is calculated in #L302 and then added to totalTVL
in #L338.
However, an error occurs in the calculation because the wrong token address is used in the getTokenBalanceFromStrategy()
function.
function getTokenBalanceFromStrategy(IERC20 token) external view returns (uint256) { return 329: queuedShares[address(this)] == 0 ? tokenStrategyMapping[token].userUnderlyingView(address(this)) : tokenStrategyMapping[token].userUnderlyingView(address(this)) + tokenStrategyMapping[token].sharesToUnderlyingView( queuedShares[address(token)] ); }
The queuedShares
is mapping of the token shares in the EigenLayer's withdrawal queue in here.
As you can see, the address used in #L329 is not the address of the specified token, but address(this)
, that is, the address of the OperatorDelegator
itself.
Therefore, Always queuedShares[address(this)] = 0
and as a result, the underlying token amount from the amount of shares is not included. That is, totalTVL
becomes much smaller in the calculateTVLs()
function.
As a result, the ezETH token can be mint or withdraw at invalid prices.
Manual Review
Add the following line in the OperatorDelegator.sol#getTokenBalanceFromStrategy()
function.
function getTokenBalanceFromStrategy(IERC20 token) external view returns (uint256) { return --- queuedShares[address(this)] == 0 +++ queuedShares[address(token)] == 0 ? tokenStrategyMapping[token].userUnderlyingView(address(this)) : tokenStrategyMapping[token].userUnderlyingView(address(this)) + tokenStrategyMapping[token].sharesToUnderlyingView( queuedShares[address(token)] ); }
Invalid Validation
#0 - c4-judge
2024-05-16T10:44:33Z
alcueca marked the issue as satisfactory
🌟 Selected for report: pauliax
Also found by: 0rpse, 0x73696d616f, 0xAadi, 0xCiphky, 0xPwned, 0xhacksmithh, 0xnev, 0xnightfall, 0xordersol, 14si2o_Flint, Aamir, Aymen0909, BiasedMerc, DanielArmstrong, Fassi_Security, FastChecker, GoatedAudits, Greed, KupiaSec, LessDupes, Maroutis, NentoR, OMEN, SBSecurity, Stefanov, TheFabled, adam-idarrha, ak1, aman, araj, aslanbek, b0g0, baz1ka, bigtone, blutorque, carlitox477, carrotsmuggler, crypticdefense, eeshenggoh, fyamf, gesha17, gjaldon, grearlake, guhu95, honey-k12, hunter_w3b, ilchovski, josephdara, kinda_very_good, lanrebayode77, m_Rassska, maxim371, mt030d, mussucal, oakcobalt, p0wd3r, peanuts, rbserver, shui, siguint, t0x1c, tapir, twcctop, ustazz, xg, zhaojohnson, zigtur, zzykxx
0.0026 USDC - $0.00
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L274-L358
The ezETH token can be mint or withdraw at invalid prices because the index of collateralToken
is specified incorrectly when calculating the token value of withdraw queue in the RestakeManager.sol#calculateTVLs()
function.
The RestakeManager.sol#calculateTVLs()
function is a function that calculates TVL.
function calculateTVLs() public view returns (uint256[][] memory, uint256[] memory, uint256) { SNIP... // withdrawalQueue total value 283: uint256 totalWithdrawalQueueValue = 0; for (uint256 i = 0; i < odLength; ) { SNIP... // Iterate through the tokens and get the value of each uint256 tokenLength = collateralTokens.length; for (uint256 j = 0; j < tokenLength; ) { SNIP... // record token value of withdraw queue 316: if (!withdrawQueueTokenBalanceRecorded) { 317: totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( 318: collateralTokens[i], 319: collateralTokens[j].balanceOf(withdrawQueue) 320: ); 321: } unchecked { ++j; } } SNIP... // Set withdrawQueueTokenBalanceRecorded flag to true 344: withdrawQueueTokenBalanceRecorded = true; unchecked { ++i; } } SNIP... // Add native ETH help in withdraw Queue and totalWithdrawalQueueValue to totalTVL totalTVL += (address(withdrawQueue).balance + totalWithdrawalQueueValue); return (operatorDelegatorTokenTVLs, operatorDelegatorTVLs, totalTVL); }
In here, The code snippet in #L316~#L321 is the part that calculates the token value of the withdraw queue.
Also, this code snippet is triggered only when i=0 by #L283 and #L344.
Therefore, collateralTokens[i]
in #L318 always specifies the first token in the token array.
On the other hand, let's look at RenzoOracle.sol#lookupTokenValue()
.
function lookupTokenValue(IERC20 _token, uint256 _balance) public view returns (uint256) { AggregatorV3Interface oracle = tokenOracleLookup[_token]; if (address(oracle) == address(0x0)) revert OracleNotFound(); (, int256 price, , uint256 timestamp, ) = oracle.latestRoundData(); if (timestamp < block.timestamp - MAX_TIME_WINDOW) revert OraclePriceExpired(); if (price <= 0) revert InvalidOraclePrice(); // Price is times 10**18 ensure value amount is scaled return (uint256(price) * _balance) / SCALE_FACTOR; }
As you can see on the right, the token value is calculated based on the price of the specified token.
As a result, the value of all tokens in the withdraw queue is incorrectly calculated as the price of collateralToken[0]
by #L318.
Therefore, the ezETH token can be mint or withdraw at invalid prices.
Manual Review
Modify the code snippet of #L316~#L321 as follows.
function calculateTVLs() public view returns (uint256[][] memory, uint256[] memory, uint256) { SNIP... // record token value of withdraw queue if (!withdrawQueueTokenBalanceRecorded) { totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( --- collateralTokens[i], +++ collateralTokens[j], collateralTokens[j].balanceOf(withdrawQueue) ); } SNIP... }
Other
#0 - c4-judge
2024-05-16T10:36:56Z
alcueca marked the issue as satisfactory
#1 - c4-judge
2024-05-16T10:38:47Z
alcueca changed the severity to 2 (Med Risk)
#2 - c4-judge
2024-05-16T10:39:08Z
alcueca changed the severity to 3 (High Risk)
#3 - c4-judge
2024-05-20T04:26:26Z
alcueca changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-05-23T13:47:20Z
alcueca changed the severity to 3 (High Risk)
🌟 Selected for report: zigtur
Also found by: 0x73696d616f, 0xBeastBoy, 0xCiphky, Aymen0909, FastChecker, LessDupes, NentoR, Sathish9098, TECHFUND, TheFabled, ak1, bigtone, cu5t0mpeo, eeshenggoh, guhu95, ilchovski, josephdara, ladboy233, mt030d, oakcobalt, rbserver, t0x1c, tapir, xg
2.6973 USDC - $2.70
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Withdraw/WithdrawQueue.sol#L206 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Withdraw/WithdrawQueue.sol#L139-L141
If the protocol pauses withdrawal in an emergency situation due to a lack of modifier in the WithdrawQueue.sol#withdraw()
function, the protocol may suffer a unexpected loss of funds.
In WithdrawQueue
, the protocol can pause withdrawals in emergency situations.
function pause() external onlyWithdrawQueueAdmin { _pause(); }
However, the whenNotPaused()
modifier is not defined in the WithdrawQueue.sol#withdraw()
function.
function withdraw(uint256 _amount, address _assetOut) external nonReentrant
Therefore, even if the protocol pauses withdraw()
in an emergency situation, the withdraw()
function is executed.
Ultimately, this may result in unexpected loss of funds in the protocol.
Manual Review
Add the whenNotPaused()
modifier to the WithdrawQueue.sol#withdraw()
function.
Other
#0 - c4-judge
2024-05-16T10:51:00Z
alcueca marked the issue as satisfactory
🌟 Selected for report: t0x1c
Also found by: 0xCiphky, 0xDemon, Bauchibred, DanielArmstrong, FastChecker, MSaptarshi, Maroutis, NentoR, Ocean_Sky, PNS, Rhaydden, SBSecurity, Shaheen, Tigerfrake, ZanyBonzy, atoko, btk, carlitox477, crypticdefense, honey-k12, hunter_w3b, ilchovski, jokr, ladboy233, rbserver, twcctop, umarkhatab_465
1.479 USDC - $1.48
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L491-L576 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Withdraw/WithdrawQueue.sol#L206-L263
The lack of slippage control for deposit()
, withdraw()
function can lead to a loss of assets for the affected users.
The RestakeManager.sol#deposit()
and WithdrawQueue.sol#withdraw()
functions call the RenzoOracle.sol#calculateMintAmount()
and RenzoOracle.sol#calculateRedeemAmount()
functions to calculate the amount of shares to be minted or assets to be withdrawed.
RestakeManager.sol#deposit()
:
function deposit( IERC20 _collateralToken, uint256 _amount, uint256 _referralId ) public nonReentrant notPaused { SNIP... // Calculate how much ezETH to mint 565: uint256 ezETHToMint = renzoOracle.calculateMintAmount( 566: totalTVL, 567: collateralTokenValue, 568: ezETH.totalSupply() 569: ); // Mint the ezETH 572: ezETH.mint(msg.sender, ezETHToMint); // Emit the deposit event emit Deposit(msg.sender, _collateralToken, _amount, ezETHToMint, _referralId); }
And then, the formula that used in RenzoOracle.sol#calculateMintAmount()
as follows.
ezETHToMint = collateralTokenValue * ezETH.totalSupply() / totalTVL
WithdrawQueue.sol#withdraw()
:
function withdraw(uint256 _amount, address _assetOut) external nonReentrant { SNIP... // transfer ezETH tokens to this address IERC20(address(ezETH)).safeTransferFrom(msg.sender, address(this), _amount); // calculate totalTVL 217: (, , uint256 totalTVL) = restakeManager.calculateTVLs(); // Calculate amount to Redeem in ETH 220: uint256 amountToRedeem = renzoOracle.calculateRedeemAmount( 221: _amount, 222: ezETH.totalSupply(), 223: totalTVL 224: ); // update amount in claim asset, if claim asset is not ETH if (_assetOut != IS_NATIVE) { // Get ERC20 asset equivalent amount amountToRedeem = renzoOracle.lookupTokenAmountFromValue( IERC20(_assetOut), amountToRedeem ); } // revert if amount to redeem is greater than withdrawBufferTarget if (amountToRedeem > getAvailableToWithdraw(_assetOut)) revert NotEnoughWithdrawBuffer(); SNIP... }
And then, the formula that used in RenzoOracle.sol#calculateRedeemAmount()
as follows.
amountToRedeem = _amount * totalTVL / ezETH.totalSupply()
When deposit, the user will deposit underlying assets (e.g., ETH) to the DepositQueue
contract, and the nativeRestakeAdmin
will restake to the EigenLayer. The number of shares minted is depending on the current mint rate of the protocol(i.e. totalTVL / ezETH.totalSupply
). The current mint rate can increase or decrease at any time, depending on the current on-chain condition when the transaction is executed.
For instance, if the transaction that deposit/withdraw mass amount before the transaction is excuted, the current mint rate can increase or decrease. Thus, one cannot ensure the result from the off-chain simulation will be the same as the on-chain execution.
Having said that, the number of shared minted will vary (larger or smaller than expected) if there is a change in the current mint rate. Assuming that Alice determined off-chain that depositing 100 ETH would issue $x$ amount of ezETH. When she executes the TX, the scale increases, leading to the amount of PT/YT issued being less than $x$. The slippage is more than what she can accept.
This slippage also applies to withdrawals.
In summary, the deposit/withdraw
function lacks the slippage control that allows the users to revert if the amount of ezETH or underlying assets they received is less than the amount they expected.
Manual Review
Implement slippage control in RestakeManager.sol#deposit()
and WithdrawQueue.sol#withdraw()
functions.
MEV
#0 - jatinj615
2024-05-14T15:46:34Z
Expected Behaviour.
#1 - C4-Staff
2024-05-15T14:38:46Z
CloudEllie marked the issue as primary issue
#2 - alcueca
2024-05-17T13:25:40Z
I disagree that this can be expected behaviour. I assume that the sponsor means that they believe that the users will find it acceptable, and the sponsor acknowledges the issue without intending to fix it.
#3 - c4-judge
2024-05-17T13:29:11Z
alcueca marked the issue as satisfactory
#4 - c4-judge
2024-05-17T13:29:14Z
alcueca marked the issue as selected for report
#5 - c4-judge
2024-05-17T13:44:25Z
alcueca marked issue #484 as primary and marked this issue as a duplicate of 484
🌟 Selected for report: 0xCiphky
Also found by: 0rpse, 0x007, 0xAadi, 14si2o_Flint, ADM, Aamir, Aymen0909, BiasedMerc, DanielArmstrong, Fassi_Security, FastChecker, KupiaSec, LessDupes, MaslarovK, Neon2835, RamenPeople, SBSecurity, Shaheen, Tendency, ZanyBonzy, adam-idarrha, araj, b0g0, baz1ka, bigtone, bill, blutorque, carrotsmuggler, cu5t0mpeo, fyamf, gesha17, gumgumzum, hunter_w3b, inzinko, jokr, josephdara, kennedy1030, kinda_very_good, lanrebayode77, m_Rassska, mt030d, mussucal, tapir, underdog, xg, zzykxx
0.0402 USDC - $0.04
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L491-L576 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Delegation/OperatorDelegator.sol#L143-L154
Assets amount smaller than the empty withdraw buffer amount cannot be deposited.
The RestakeManager.sol#deposit()
function checks the restake buffer and fills it if it is lower than the buffer target.
function deposit( IERC20 _collateralToken, uint256 _amount, uint256 _referralId ) public nonReentrant notPaused { SNIP... // Transfer the collateral token to this address _collateralToken.safeTransferFrom(msg.sender, address(this), _amount); // Check the withdraw buffer and fill if below buffer target 543: uint256 bufferToFill = depositQueue.withdrawQueue().getBufferDeficit( address(_collateralToken) ); if (bufferToFill > 0) { 547: bufferToFill = (_amount <= bufferToFill) ? _amount : bufferToFill; // update amount to send to the operator Delegator 549: _amount -= bufferToFill; // safe Approve for depositQueue _collateralToken.safeApprove(address(depositQueue), bufferToFill); // fill Withdraw Buffer via depositQueue depositQueue.fillERC20withdrawBuffer(address(_collateralToken), bufferToFill); } // Approve the tokens to the operator delegator 559: _collateralToken.safeApprove(address(operatorDelegator), _amount); // Call deposit on the operator delegator 562: operatorDelegator.deposit(_collateralToken, _amount); SNIP... }
In #L543, bufferToFill
represents an empty withdrawal buffer for a specific asset.
If the deposit amount (_amount
) is less than bufferTofill
in #L547, bufferTofill
returns _amount
.
As a result, _amount
returns 0 in #L549.
Therefore, OperatorDelegator.sol#deposit()
function will be reverted in #L562.
function deposit( IERC20 token, uint256 tokenAmount ) external nonReentrant onlyRestakeManager returns (uint256 shares) { 147: if (address(tokenStrategyMapping[token]) == address(0x0) || tokenAmount == 0) 148: revert InvalidZeroInput(); // Move the tokens into this contract token.safeTransferFrom(msg.sender, address(this), tokenAmount); return _deposit(token, tokenAmount); }
As a result, Assets amount smaller than the empty withdraw buffer amount cannot be deposited.
Manual Review
Add following lines in the RestakeManager.sol#deposit()
function.
function deposit( IERC20 _collateralToken, uint256 _amount, uint256 _referralId ) public nonReentrant notPaused { SNIP... // Transfer the collateral token to this address _collateralToken.safeTransferFrom(msg.sender, address(this), _amount); // Check the withdraw buffer and fill if below buffer target uint256 bufferToFill = depositQueue.withdrawQueue().getBufferDeficit( address(_collateralToken) ); if (bufferToFill > 0) { bufferToFill = (_amount <= bufferToFill) ? _amount : bufferToFill; // update amount to send to the operator Delegator _amount -= bufferToFill; // safe Approve for depositQueue _collateralToken.safeApprove(address(depositQueue), bufferToFill); // fill Withdraw Buffer via depositQueue depositQueue.fillERC20withdrawBuffer(address(_collateralToken), bufferToFill); } // Approve the tokens to the operator delegator _collateralToken.safeApprove(address(operatorDelegator), _amount); // Call deposit on the operator delegator --- operatorDelegator.deposit(_collateralToken, _amount); +++ if(_amount > 0) +++ operatorDelegator.deposit(_collateralToken, _amount); SNIP... }
DoS
#0 - c4-judge
2024-05-20T05:02:35Z
alcueca marked the issue as satisfactory
🌟 Selected for report: Sathish9098
Also found by: 0x73696d616f, 0xCiphky, 0xmystery, ABAIKUNANBAEV, Bauchibred, BiasedMerc, Fassi_Security, FastChecker, GalloDaSballo, GoatedAudits, K42, KupiaSec, LessDupes, Limbooo, ReadyPlayer2, Rhaydden, SBSecurity, Sabit, Sparrow, WildSniper, ZanyBonzy, adam-idarrha, adeolu, araj, aslanbek, atoko, b0g0, carlitox477, crypticdefense, fyamf, gesha17, gjaldon, grearlake, gumgumzum, hihen, honey-k12, hunter_w3b, inzinko, jesjupyter, jokr, kennedy1030, kind0dev, kinda_very_good, ladboy233, lanrebayode77, oakcobalt, oualidpro, pauliax, rbserver, t0x1c, tapir, underdog, xg, zzykxx
0 USDC - $0.00
https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/RestakeManager.sol#L274-L358 https://github.com/code-423n4/2024-04-renzo/blob/main/contracts/Deposits/DepositQueue.sol#L254
In the RestakeManager.sol#calculateTVLs()
function, the ERC20 balance of the deposit queue is not included in the TVL calculation.
Therefore, an attacker can hold ezETH at a low price by issuing ezETH before the DepositQueue.sol#sweepERC20()
function is executed.
ReadME states:
ERC20 rewards earned in EigenLayer will be sent to the DepositQueue address. The protocol can then sweep the ERC20 fee percentage to the target address and deposit the remaining ERC20 tokens into the protocol through an Operator Delegator.
That is, ERC20_REWARD_ADMIN
sweeps any accumulated ERC20 tokens in DepositQueue contract to the RestakeManager by using DepositQueue.sol#sweepERC20()
function.
function sweepERC20(IERC20 token) external onlyERC20RewardsAdmin { ... }
As a result, ERC20 rewards are locked in the DepositQueue
contract until the sweepERC20()
function is called.
However, when calculating TVL in the calculateTVLs()
function, only the ERC20 token balance of the WithdrawQueue
contract is included, and the balance of the DepositQueue
contract is not included.
function calculateTVLs() public view returns (uint256[][] memory, uint256[] memory, uint256) { ... for (uint256 i = 0; i < odLength; ) { ... for (uint256 j = 0; j < tokenLength; ) { ... // record token value of withdraw queue 316: if (!withdrawQueueTokenBalanceRecorded) { 317: totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( 318: collateralTokens[i], 319: collateralTokens[j].balanceOf(withdrawQueue) 320: ); 321: } ... } ... } // Add native ETH help in withdraw Queue and totalWithdrawalQueueValue to totalTVL totalTVL += (address(withdrawQueue).balance + totalWithdrawalQueueValue); return (operatorDelegatorTokenTVLs, operatorDelegatorTVLs, totalTVL); }
Therefore, an attacker can hold ezETH at a low price by issuing ezETH before the DepositQueue.sol#sweepERC20()
function is executed.
Manual Review
Add following lines in the RestakeManager.sol#calculateTVLs()
function.
```solidity function calculateTVLs() public view returns (uint256[][] memory, uint256[] memory, uint256) { ... for (uint256 i = 0; i < odLength; ) { ... for (uint256 j = 0; j < tokenLength; ) { ... // record token value of withdraw queue if (!withdrawQueueTokenBalanceRecorded) { totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( collateralTokens[i], collateralTokens[j].balanceOf(withdrawQueue) ); +++ totalWithdrawalQueueValue += renzoOracle.lookupTokenValue( +++ collateralTokens[j], +++ collateralTokens[j].balanceOf(depositQueue) +++ ); } ... } ... } // Add native ETH help in withdraw Queue and totalWithdrawalQueueValue to totalTVL totalTVL += (address(withdrawQueue).balance + totalWithdrawalQueueValue); return (operatorDelegatorTokenTVLs, operatorDelegatorTVLs, totalTVL); }
## Assessed type Other
#0 - jatinj615
2024-05-13T23:00:03Z
EigenLayer does not provide rewards in ERC20 as the payments system is not finalised. The sweepERC20 function is present to allow admin to sweep any supported ERC20 collateral periodically that is sent to depositQueue intentionally/unintentionally which can then be considered as rewards.
#1 - C4-Staff
2024-05-15T14:42:50Z
CloudEllie marked the issue as primary issue
#2 - alcueca
2024-05-16T05:56:07Z
The sweepERC20 function exists, and the attack vector is doable, even if infrequent. Downgrading to Medium since the source of funds is not defined.
#3 - c4-judge
2024-05-16T05:56:54Z
alcueca changed the severity to 2 (Med Risk)
#4 - c4-judge
2024-05-16T05:58:01Z
alcueca marked the issue as satisfactory
#5 - c4-judge
2024-05-16T06:00:13Z
alcueca marked issue #383 as primary and marked this issue as a duplicate of 383
#6 - c4-judge
2024-05-27T09:30:28Z
alcueca changed the severity to QA (Quality Assurance)
#7 - c4-judge
2024-05-28T11:01:24Z
alcueca marked the issue as grade-b