Platform: Code4rena
Start Date: 16/02/2023
Pot Size: $144,750 USDC
Total HM: 17
Participants: 154
Period: 19 days
Judge: Trust
Total Solo HM: 5
Id: 216
League: ETH
Rank: 39/154
Findings: 2
Award: $370.05
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: GalloDaSballo
Also found by: 0x3b, 0xAgro, 0xSmartContract, 0xTheC0der, 0xackermann, 0xnev, 0xsomeone, ABA, BRONZEDISC, Bjorn_bug, Bnke0x0, Breeje, Co0nan, CodeFoxInc, CodingNameKiki, DadeKuma, DeFiHackLabs, IceBear, Josiah, Kaysoft, Lavishq, MohammedRizwan, PaludoX0, PawelK, Phantasmagoria, Raiders, RaymondFam, Rickard, Rolezn, Sathish9098, SleepingBugs, SuperRayss, UdarTeam, Udsen, Viktor_Cortess, arialblack14, ast3ros, bin2chen, brgltd, btk, catellatech, ch0bu, chaduke, chrisdior4, codeislight, cryptonue, delfin454000, descharre, dontonka, emmac002, fs0c, hacker-dom, hansfriese, imare, lukris02, luxartvinsec, martin, matrix_0wl, peanuts, rbserver, shark, tnevler, trustindistrust, tsvetanovv, vagrant, yongskiws, zzzitron
61.2601 USDC - $61.26
pragma solidity 0.6.11;
\Ethos-Core\contracts\CollateralConfig.sol \Ethos-Core\contracts\BorrowerOperations.sol \Ethos-Core\contracts\TroveManager.sol \Ethos-Core\contracts\ActivePool.sol \Ethos-Core\contracts\StabilityPool.sol \Ethos-Core\contracts\LQTY\CommunityIssuance.sol \Ethos-Core\contracts\LQTY\LQTYStaking.sol \Ethos-Core\contracts\LUSDToken.sol
It's a best practice to use the latest compiler version. The specified minimum compiler version is not up to date. Older compilers might be susceptible to some bugs. We recommend changing the solidity version pragma to the latest version to enforce the use of an up-to-date compiler.
A list of known compiler bugs and their severity can be found here: https://etherscan.io/solcbuginfo
Consider using only one approach throughout the codebase, e.g. only uint or only uint256.
\Ethos-Core\contracts\TroveManager.sol
48: uint constant public SECONDS_IN_ONE_MINUTE = 60;
The value of 1 minute can be used directly.
It’s possible to name the imports to improve code readability for all files in scope.
\Ethos-Core\contracts\ActivePool.sol
194: ActivePoolLUSDDebtUpdated(_collateral, LUSDDebt[_collateral]); 201: ActivePoolLUSDDebtUpdated(_collateral, LUSDDebt[_collateral]);
\Ethos-Vault\contracts\ReaperVaultV2.sol
155: require(_feeBPS <= PERCENT_DIVISOR / 5, "Fee cannot be higher than 20 BPS"); 181: require(_feeBPS <= PERCENT_DIVISOR / 5, "Fee cannot be higher than 20 BPS"); PERCENT_DIVISOR = 10000 = 100%, so 10000/5 = 2000 = 20%. In require we see: 20BPS = 0.2 %
There is a difference between constant variables and immutable variables, and they should each be used in their appropriate contexts. 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. 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.
\Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol
bytes32 public constant KEEPER = keccak256("KEEPER"); bytes32 public constant STRATEGIST = keccak256("STRATEGIST"); bytes32 public constant GUARDIAN = keccak256("GUARDIAN"); bytes32 public constant ADMIN = keccak256("ADMIN");
\Ethos-Vault\contracts\ReaperVaultV2.sol
bytes32 public constant DEPOSITOR = keccak256("DEPOSITOR"); bytes32 public constant STRATEGIST = keccak256("STRATEGIST"); bytes32 public constant GUARDIAN = keccak256("GUARDIAN"); bytes32 public constant ADMIN = keccak256("ADMIN");
\Ethos-Core\contracts\TroveManager.sol
14: // import "./Dependencies/Ownable.sol"; 18: contract TroveManager is LiquityBase, /*Ownable,*/ CheckContract, ITroveManager { // string constant public NAME = "TroveManager"; 431: // if (_ICR >= _MCR && ( _ICR >= _TCR || singleLiquidation.entireTroveDebt > _LUSDInStabPool)) 539: // if !vars.recoveryModeAtStart
\Ethos-Vault\contracts\ReaperVaultV2.sol \Ethos-Vault\contracts\ReaperVaultERC4626.sol \Ethos-Vault\contracts\ReaperStrategyGranarySupplyOnly.sol \Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol
pragma solidity ^0.8.0;
Recommendation Locking the pragma helps to ensure that contracts do not accidentally get deployed using an outdated compiler version.
Note that pragma statements can be allowed to float when a contract is intended for consumption by other developers, as in the case of contracts in a library or a package.
initialize() function can be called by anybody when the contract is not initialized.
\Ethos-Vault\contracts\ReaperStrategyGranarySupplyOnly.sol
62: function initialize( address _vault, address[] memory _strategists, address[] memory _multisigRoles, IAToken _gWant ) public initializer { gWant = _gWant; want = _gWant.UNDERLYING_ASSET_ADDRESS(); __ReaperBaseStrategy_init(_vault, want, _strategists, _multisigRoles); rewardClaimingTokens = [address(_gWant)]; }
Before solidity version 0.8.0, hitting an assert consumes the remainder of the transaction’s available gas rather than returning it, as require()/revert() do. assert() should be avoided even past solidity version 0.8.0 as its documentation states that “The assert function creates an error of type Panic(uint256). … Properly functioning code should never create a Panic, not even on invalid external input. If this happens, then there is a bug in your contract that you should fix”.
\Ethos-Core\contracts\BorrowerOperations.sol
128: assert(MIN_NET_DEBT > 0); 197: assert(vars.compositeDebt > 0); 301: assert(msg.sender == _borrower || (msg.sender == stabilityPoolAddress && _collTopUp > 0 && _LUSDChange == 0)); 331: assert(_collWithdrawal <= vars.coll);
\Ethos-Core\contracts\StabilityPool.sol
526: assert(_debtToOffset <= _totalLUSDDeposits); 551: assert(_LUSDLossPerUnitStaked <= DECIMAL_PRECISION); 591: assert(newP > 0);
\Ethos-Core\contracts\TroveManager.sol
417: assert(_LUSDInStabPool != 0); 1225: assert(totalStakesSnapshot[_collateral] > 0); 1280: assert(closedStatus != Status.nonExistent && closedStatus != Status.active); 1343: assert(troveStatus != Status.nonExistent && troveStatus != Status.active); 1349: assert(index <= idxLast); 1415: assert(newBaseRate > 0); 1490: assert(decayedBaseRate <= DECIMAL_PRECISION);
\Ethos-Core\contracts\LUSDToken.sol
The ecrecover function is used to verify and execute Meta transactions. The built-in EVM precompile ecrecover is susceptible to signature malleability (because of non-unique s and v values) which could lead to replay attacks. While this is not exploitable for replay attacks in the current implementation because of the use of nonces, this may become a vulnerability if used elsewhere.
Recommend considering using OpenZeppelin’s ECDSA library (which prevents this malleability) instead of the built-in function.
/Ethos-Vault/contracts/ReaperVaultV2.sol#L178-L184
It is a good practice to give time for users to react and adjust to critical changes. A timelock provides more guarantees and reduces the level of trust required, thus decreasing the risk for users. It also indicates that the project is legitimate. However, it appears that no timelock capabilities have been utilized, which could have a significant impact on multiple users, prompting them to respond or receive advance notifications.
On several locations in the code precautions are not being taken for not dividing by 0, this will revert the code. These functions can be called with a 0 value in the input, this value is not checked for being bigger than 0, which that means in some scenarios can potentially trigger a division by zero.
\Ethos-Vault\contracts\ReaperVaultV2.sol
347: shares = (_amount * totalSupply()) / freeFunds; 472: uint256 bpsChange = Math.min((loss * totalAllocBPS) / totalAllocated, stratParams.allocBPS); 499: uint256 shares = supply == 0 ? performanceFee : (performanceFee * supply) / _freeFunds();
\Ethos-Vault\contracts\ReaperStrategyGranarySupplyOnly.sol
62: function initialize( address _vault, address[] memory _strategists, address[] memory _multisigRoles, IAToken _gWant ) public initializer { gWant = _gWant; want = _gWant.UNDERLYING_ASSET_ADDRESS(); __ReaperBaseStrategy_init(_vault, want, _strategists, _multisigRoles); rewardClaimingTokens = [address(_gWant)]; }
E:\audits\2023-02-ethos\Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol
168: function setEmergencyExit() external { _atLeastRole(GUARDIAN); emergencyExit = true; IVault(vault).revokeStrategy(address(this)); }
#0 - c4-judge
2023-03-10T10:13:12Z
trust1995 marked the issue as grade-b
#1 - trust1995
2023-03-10T10:13:18Z
Barely B.
#2 - c4-sponsor
2023-03-28T21:33:59Z
0xBebis marked the issue as sponsor acknowledged
🌟 Selected for report: c3phas
Also found by: 0x3b, 0x6980, 0x73696d616f, 0xSmartContract, 0xackermann, 0xhacksmithh, 0xsomeone, Bnke0x0, Bough, Budaghyan, Darshan, DeFiHackLabs, Deivitto, GalloDaSballo, JCN, LethL, Madalad, MiniGlome, Morraez, P-384, PaludoX0, Phantasmagoria, Praise, RHaO-sec, Rageur, RaymondFam, ReyAdmirado, Rickard, Rolezn, SaeedAlipoor01988, Saintcode_, Sathish9098, TheSavageTeddy, Tomio, Viktor_Cortess, abiih, arialblack14, atharvasama, banky, codeislight, cryptonue, ddimitrov22, dec3ntraliz3d, descharre, dharma09, emmac002, favelanky, hl_, hunter_w3b, kaden, kodyvim, matrix_0wl, oyc_109, pavankv, scokaf, seeu, yamapyblack
308.7866 USDC - $308.79
\Ethos-Vault\contracts\ReaperVaultV2.sol
168: totalAllocBPS += _allocBPS; 214: totalAllocBPS -= strategies[_strategy].allocBPS; 396: totalAllocated -= actualWithdrawn; 445: totalAllocBPS -= bpsChange; 452: totalAllocated -= loss;
if you remove the return statement at the end of the function and instead return the variable directly from the function definition, it will save some gas. This is because you would be avoiding the unnecessary assignment operation and just directly returning the value.
\Ethos-Vault\contracts\ReaperVaultV2.sol
462: function _chargeFees(address strategy, uint256 gain) internal returns (uint256 + performanceFee ) { uint256 performanceFee = (gain * strategies[strategy].feeBPS) / PERCENT_DIVISOR; if (performanceFee != 0) { uint256 supply = totalSupply(); uint256 shares = supply == 0 ? performanceFee : (performanceFee * supply) / _freeFunds(); _mint(treasury, shares); } - return performanceFee; }
Similar cases:
659: function _cascadingAccessRoles() internal view override returns (bytes32[] memory) {
\Ethos-Vault\contracts\ReaperStrategyGranarySupplyOnly.sol
230: function balanceOfPool() public view returns (uint256) {
\Ethos-Core\contracts\TroveManager.sol
353: return singleLiquidation; //can be omitted 436: return singleLiquidation; //can be omitted 913: return newTotals; //can be omitted 1046: function getNominalICR(address _borrower, address _collateral) public view override returns (uint) { 1059: ) public view override returns (uint) { 1067: function _getCurrentTroveAmounts(address _borrower, address _collateral) internal view returns (uint, uint) { 1124: function getPendingCollateralReward(address _borrower, address _collateral) public view override returns (unit) 1139: function getPendingLUSDDebtReward(address _borrower, address _collateral) public view override returns (uint) { 1202: function _updateStakeAndTotalStakes(address _borrower, address _collateral) internal returns (uint) { 1214: function _computeNewStake(address _collateral, uint _coll) internal view returns (uint) { 1333: return index; //can be omitted 1403: ) external override returns (uint) { 1423: return newBaseRate; //can be omitted 1449: function _calcRedemptionFee(uint _redemptionRate, uint _collateralDrawn) internal pure returns (uint) { 1568: function increaseTroveColl(address _borrower, address _collateral, uint _collIncrease) external override returns (uint) { 1575: function decreaseTroveColl(address _borrower, address _collateral, uint _collDecrease) external override returns (uint) { 1583: function increaseTroveDebt(address _borrower, address _collateral, uint _debtIncrease) external override returns (uint) { 1590: function decreaseTroveDebt(address _borrower, address _collateral, uint _debtDecrease) external override returns (uint) {
\Ethos-Core\contracts\StabilityPool.sol
439: function _computeLQTYPerUnitStaked(uint _LQTYIssuance, uint _totalLUSDDeposits) internal returns (uint) { 543: return (collGainPerUnitStaked, LUSDLossPerUnitStaked); //can be omitted 683: function getDepositorLQTYGain(address _depositor) public view override returns (uint) { 693: function _getLQTYGainFromSnapshots(uint initialDeposit, Snapshots memory snapshots) internal view returns (uint) { 718: function getCompoundedLUSDDeposit(address _depositor) public view override returns (uint) { 735: returns (unit)
\Ethos-Core\contracts\LQTY\LQTYStaking.sol
217: function _getPendingLUSDGain(address _user) internal view returns (uint) {
\Ethos-Core\contracts\BorrowerOperations.sol
432: function _getUSDValue(uint _coll, uint _price, uint256 _collDecimals) internal pure returns (uint) { 466: returns (uint, unit) 673: returns (unit) 695: returns (unit) 713: returns (uint, unit) 736: returns (uint)
\Ethos-Vault\contracts\ReaperVaultV2.sol
263: delete withdrawalQueue;
\Ethos-Vault\contracts\ReaperStrategyGranarySupplyOnly.sol
162: delete steps;
for arrays, setting the length to zero (withdrawalQueue.length = 0) is cheaper than using delete withdrawalQueue because the delete keyword also clears the storage slot for the array, which is an additional gas cost.
By doing these checks first, the function is able to revert before gas in a function that may ultimately revert in the unhappy case.
\Ethos-Vault\contracts\ReaperVaultV2.sol
627: function updateTreasury(address newTreasury) external { _atLeastRole(DEFAULT_ADMIN_ROLE); require(newTreasury != address(0), "Invalid address"); //move require up to the first line of the function 637: function inCaseTokensGetStuck(address _token) external { _atLeastRole(ADMIN); require(_token != address(token), "!token"); //move require up to the first line of the function
\Ethos-Core\contracts\ActivePool.sol
93: require(_treasuryAddress != address(0), "Treasury cannot be 0 address"); //move require up to the first line of the function 127: require(_bps <= 10_000, "Invalid BPS value"); "); //move require up to the first line of the function
\Ethos-Core\contracts\CollateralConfig.sol
66: require(_MCRs[i] >= MIN_ALLOWED_MCR, "MCR below allowed minimum"); 69: require(_CCRs[i] >= MIN_ALLOWED_CCR, "CCR below allowed minimum");
\Ethos-Core\contracts\CollateralConfig.sol
94: require(_MCR >= MIN_ALLOWED_MCR, "MCR below allowed minimum"); 97: require(_CCR >= MIN_ALLOWED_CCR, "CCR below allowed minimum");
When we use two separate require statements, the EVM will execute two JUMPI and REVERT instructions if either of the conditions is not met. However, when you use require(condition1 || condition2, "error"), the EVM will only execute one JUMPI instruction, and then another JUMPI instruction to skip the next REVERT instruction if either of the conditions is met. This can save some gas compared to using two separate require statements.
\Ethos-Core\contracts\CollateralConfig.sol
53: - require(_MCRs.length == _collaterals.length, "Array lengths must match"); - require(_CCRs.length == _collaterals.length, "Array lenghts must match"); + require(_MCRs.length == _collaterals.length || _CCRs.length == _collaterals.length , "Array lengths must match");
\Ethos-Core\contracts\BorrowerOperations.sol
There are different error messages in require statements in the following code, but in theory, they could be combined the same way as in the previous example.
529: require(IERC20(_collateral).balanceOf(_user) >= _collAmount, "BorrowerOperations: Insufficient user collateral balance"); require(IERC20(_collateral).allowance(_user, address(this)) >= _collAmount, "BorrowerOperations: Insufficient collateral allowance");
If you can limit the length to a certain number of bytes, always use one of bytes1 to bytes32 because they are much cheaper.
\Ethos-Core\contracts\ActivePool.sol
30: string constant public NAME = "ActivePool"
\Ethos-Core\contracts\BorrowerOperations.sol
21: string constant public NAME = "BorrowerOperations";
\Ethos-Core\contracts\LUSDToken.sol
32: string constant internal _NAME = "LUSD Stablecoin"; string constant internal _SYMBOL = "LUSD"; string constant internal _VERSION = "1";
\Ethos-Core\contracts\StabilityPool.sol
150: string constant public NAME = "StabilityPool";
\Ethos-Core\contracts\LQTY\CommunityIssuance.sol
19: string constant public NAME = "CommunityIssuance";
\Ethos-Vault\contracts\ReaperVaultV2.sol
338: if (totalSupply() == 0) { shares = _amount; } else { shares = (_amount * totalSupply()) / freeFunds; // use "freeFunds" instead of "pool" }
Can be swapped with:
shares = (totalSupply() == 0) ? _amount : (_amount * totalSupply()) / freeFunds;
When fetching data from a storage location, assigning the data to a memory variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declaring the variable with the memory keyword, declaring the variable with the storage keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory, or if the array/struct is being read from another memory array/struct.
\Ethos-Core\contracts\ActivePool.sol
`240: LocalVariables_rebalance memory vars;`
\Ethos-Core\contracts\BorrowerOperations.sol
`ContractsCache memory contractsCache = ContractsCache(troveManager, activePool, lusdToken);` `LocalVariables_openTrove memory vars;`
\Ethos-Core\contracts\TroveManager.sol
`518: LocalVariables_OuterLiquidationFunction memory vars;` `LiquidationTotals memory totals;` 684: LocalVariables_LiquidationSequence memory vars; LiquidationValues memory singleLiquidation; 722: LocalVariables_OuterLiquidationFunction memory vars; LiquidationTotals memory totals; 797: LocalVariables_LiquidationSequence memory vars; 801: LiquidationValues memory singleLiquidation;
\Ethos-Vault\contracts\ReaperVaultV2.sol
494: LocalVariables_report memory vars;
Also if you use msg.sender several times within a function, only one CALLER opcode will be executed to set the value of msg.sender in memory, and subsequent references to msg.sender will simply read from that memory location, without incurring additional gas costs for a CALLER opcode.
\Ethos-Vault\contracts\ReaperVaultV2.sol
Before: 5128 gas
225: function availableCapital() public view returns (int256) { address stratAddr = msg.sender; if (totalAllocBPS == 0 || emergencyShutdown) { return -int256(strategies[stratAddr].allocated); } uint256 stratMaxAllocation = (strategies[stratAddr].allocBPS * balance()) / PERCENT_DIVISOR; uint256 stratCurrentAllocation = strategies[stratAddr].allocated;
After: 5114 gas
function availableCapital() public view returns (int256) { if (totalAllocBPS == 0 || emergencyShutdown) { return -int256(strategies[msg.sender].allocated); } uint256 stratMaxAllocation = (strategies[msg.sender].allocBPS * balance()) / PERCENT_DIVISOR; uint256 stratCurrentAllocation = strategies[msg.sender].allocated;
\Ethos-Vault\contracts\ReaperVaultV2.sol
Contracts most called functions could simply save gas by function ordering via Method ID. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas is added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions.
For example, during tests, I noticed that the function availableCapital() is the most often called function in the Vault contract. So, we can modify its name to save some gas.
The current function’s Signature is 199cb7d8
and as we know a function with 00 at the beginning of the signature will be the cheapest one to call.
So availableCapital()
can be renamed to availableCapital_3()
, then its signature will be 0094169d.
Similar optimization could be done for the most often callable functions from other contracts.
Make sure Solidity’s optimizer is enabled. It reduces gas costs. If you want to gas optimize for contract deployment (costs less to deploy a contract) then set the Solidity optimizer at a low number. If you want to optimize for run-time gas costs (when functions are called on a contract) then set the optimizer to a high number.
Set the optimization value higher than 800 in your hardhat.config.js file.
type(uint120).max or type(uint112).max, etc. it uses more gas in the distribution process and also for each transaction than constant usage.
\Ethos-Vault\contracts\ReaperVaultV2.sol
624: updateTvlCap(type(uint256).max);
\Ethos-Vault\contracts\ReaperVaultERC4626.sol
81: if (tvlCap == type(uint256).max) return type(uint256).max; 124: if (tvlCap == type(uint256).max) return type(uint256).max;
\Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol
79: IERC20Upgradeable(want).safeApprove(vault, type(uint256).max);
\Ethos-Vault\contracts\ReaperVaultV2.sol
111: constructor( address _token, string memory _name, string memory _symbol, uint256 _tvlCap, address _treasury, address[] memory _strategists, address[] memory _multisigRoles ) ERC20(string(_name), string(_symbol)) { token = IERC20Metadata(_token); constructionTime = block.timestamp; lastReport = block.timestamp; tvlCap = _tvlCap; - //treasury = _treasury; + assembly { sstore(treasury.slot, _treasury) } 663: function updateTreasury(address newTreasury) external { _atLeastRole(DEFAULT_ADMIN_ROLE); require(newTreasury != address(0), "Invalid address"); - treasury = newTreasury; + assembly { sstore(treasury.slot, newTreasury) } }
\Ethos-Vault\contracts\abstract\ReaperBaseStrategyv4.sol
63: function __ReaperBaseStrategy_init( address _vault, address _want, address[] memory _strategists, address[] memory _multisigRoles ) internal onlyInitializing { __UUPSUpgradeable_init(); __AccessControlEnumerable_init(); -// vault = _vault; -// want = _want; + assembly { sstore(vault.slot, _vault) sstore(want.slot, _want) } IERC20Upgradeable(want).safeApprove(vault, type(uint256).max); uint256 numStrategists = _strategists.length; for (uint256 i = 0; i < numStrategists; i = i.uncheckedInc()) { _grantRole(STRATEGIST, _strategists[i]); } require(_multisigRoles.length == 3, "Invalid number of multisig roles"); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(DEFAULT_ADMIN_ROLE, _multisigRoles[0]); _grantRole(ADMIN, _multisigRoles[1]); _grantRole(GUARDIAN, _multisigRoles[2]); clearUpgradeCooldown(); }
There are several similar cases in Ethos-Core, but with the current solidity version 0.6.11; it doesn’t work there.
You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. Making the constructor payable eliminates the need for an initial check of msg.value == 0 and saves 13 gas on deployment with no security risks.
E:\audits\2023-02-ethos\Ethos-Vault\contracts\ReaperVaultERC4626.sol
16: constructor( address _token, string memory _name, string memory _symbol, uint256 _tvlCap, address _treasury, address[] memory _strategists, address[] memory _multisigRoles ) + payable ReaperVaultV2(_token, _name, _symbol, _tvlCap, _treasury, _strategists, _multisigRoles) {}
\Ethos-Core\contracts\TroveManager.sol
225: constructor() public { // makeshift ownable implementation to circumvent contract size limit owner = msg.sender; }
\Ethos-Core\contracts\LQTY\CommunityIssuance.sol
57: constructor() public { distributionPeriod = 14 days; }
#0 - c4-judge
2023-03-09T18:43:47Z
trust1995 marked the issue as grade-a