Platform: Code4rena
Start Date: 13/02/2024
Pot Size: $24,500 USDC
Total HM: 5
Participants: 84
Period: 6 days
Judge: 0xA5DF
Id: 331
League: ETH
Rank: 15/84
Findings: 3
Award: $275.02
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: BowTiedOriole
Also found by: 0x0bserver, 0xAadi, 0xJoyBoy03, 0xlamide, 0xlemon, 0xpiken, Babylen, Breeje, Brenzee, CodeWasp, DanielArmstrong, DarkTower, Fassi_Security, Fitro, Honour, JohnSmith, Krace, MrPotatoMagic, Myrault, ReadyPlayer2, SovaSlava, SpicyMeatball, TheSavageTeddy, Tigerfrake, atoko, cryptphi, csanuragjain, d3e4, gesha17, kinda_very_good, krikolkk, matejdb, max10afternoon, miaowu, n0kto, nuthan2x, parlayan_yildizlar_takimi, peanuts, petro_1912, pontifex, psb01, pynschon, rouhsamad, shaflow2, slippopz, spark, turvy_fuzz, web3pwn, zhaojohnson
7.1828 USDC - $7.18
https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L127-L146 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L164-L179 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L198-L237
The _beforeTokenTransfer
function does not verify whether the transfer amount is zero, thereby enabling any user to add a holder to holders
by transferring zero tokens to a valid holder. The only requirement is that the valid holder must have zero balances before the transfer.
By adding holders, anyone can increase the length of holders
, potentially resulting in a denial-of-service (DoS) situation due to Out Of Gas, as both _afterTokenTransfer
and distribute
may iterate through the entire holders
.
In the _beforeTokenTransfer
function, to
is added to holders
if it is a valid holder and has a balance equal to zero before the transfer.
However, it does not check whether the transfer amount
is zero. Consequently, even if the transferred amount
is 0, as long as the balance of to
is also 0, to
will be added to holders
. Although to
is added to holders
, its balance remains zero, implying that to
could be added to holders
again.
function _beforeTokenTransfer( address from, address to, uint256 amount ) internal virtual override { require(!LockedForDistribution, "distribution in progress"); if (!(to == address(0))) { require( isApprovedHolder(to), "receiver not approved to hold the token" ); } if (from == address(0) || to == address(0)) { _beforeMintOrBurn(); } //@audit `to` is added to holders as long as its balance is zero. bool exists = (this.balanceOf(to) != 0); if (!exists) { holders.push(to); } }
This vulnerability allows anyone to exploit the situation and add elements to holders
, potentially causing a Denial-of-Service (DoS) situation due to Out of Gas, as both _afterTokenTransfer
and distribute
iterate through the entire holders
.
function _afterTokenTransfer( address from, address to, uint256 amount ) internal virtual override { bool stillHolding = (this.balanceOf(from) != 0); if (!stillHolding) { for (uint i = 0; i < holders.length; i++) { if (holders[i] == from) { // Remove the element at i by copying the last one into its place and removing the last element holders[i] = holders[holders.length - 1]; holders.pop(); } } } }
function distribute(uint256 numDistributions) public nonReentrant { require(numDistributions > 0, "must process at least 1 distribution"); if (!LockedForDistribution) { require( _isPastMinDistributionPeriod(), "MinDistributionPeriod not met" ); _beginDistribution(); } uint256 limit = Math.min( nextDistributionRecipient + numDistributions, holders.length ); uint i; for (i = nextDistributionRecipient; i < limit; i++) { address recipient = holders[i]; if (isApprovedHolder(recipient)) { uint256[] memory receipts = new uint256[]( distributableERC20s.length ); for (uint j = 0; j < distributableERC20s.length; j++) { IERC20 toDistribute = IERC20(distributableERC20s[j]); uint256 entitlement = erc20EntitlementPerUnit[j] * this.balanceOf(recipient); if (toDistribute.transfer(recipient, entitlement)) { receipts[j] = entitlement; } } emit Distribution(recipient, distributableERC20s, receipts); } } nextDistributionRecipient = i; if (nextDistributionRecipient == holders.length) { _endDistribution(); } }
To validate the aforementioned issue, I conducted a test where holder1
represents an unauthorized holder, and holder2
is an authorized holder. In this scenario, holder1
is able to continually add new holders to the holders
list by repeatedly transferring 0 tokens to holder2
.
Apply the patch and run it with npx hardhat test
.
diff --git a/liquid-infrastructure/test/liquidERC20.ts b/liquid-infrastructure/test/liquidERC20.ts index 0121500..73577d1 100644 --- a/liquid-infrastructure/test/liquidERC20.ts +++ b/liquid-infrastructure/test/liquidERC20.ts @@ -176,6 +176,31 @@ async function failToManageNFTNotOwner( ); } +async function emptyHolder( + infraERC20: LiquidInfrastructureERC20, + holder1: HardhatEthersSigner, + holder2: HardhatEthersSigner, + badSigner: HardhatEthersSigner +) { + + // Holder1 is not a valid holder + const infraERC20Holder1 = infraERC20.connect(holder1); + + // Approve holder2 + await expect(infraERC20.approveHolder(holder2.address)).to.not.be.reverted; + + // Transfer 0 erc20 from holder1 to holder2 ==> two holders + await expect(infraERC20Holder1.transfer(holder2.address, 0)).to.not.be + .reverted; + var x = await infraERC20.getHolders(); + console.log("holders length ",x); + // Transfer 0 erc20 from holder1 to holder2 again ==> three holders + await expect(infraERC20Holder1.transfer(holder2.address, 0)).to.not.be + .reverted; + x = await infraERC20.getHolders(); + console.log("holders length ",x); +} + // Checks that only owner-approved holders are allowed to hold the ERC20, // and that even the owner cannot give them tokens without approving them async function basicErc20HolderTests( @@ -529,6 +554,13 @@ describe("LiquidInfrastructureERC20 tests", function () { await basicErc20HolderTests(infraERC20, holder1, holder2, badSigner); }); + it.only("empyt holders", async function () { + const { infraERC20, holder1, holder2, badSigner } = + await liquidErc20Fixture(); + + await emptyHolder(infraERC20, holder1, holder2, badSigner); + }); + it("manages distributions (basic)", async function () { const { infraERC20,
This diff add a view function to LiquidInfrastructureERC20.sol
to get the length of holders
.
diff --git a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol index 4722279..834cf4e 100644 --- a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol +++ b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol @@ -145,6 +145,9 @@ contract LiquidInfrastructureERC20 is } } +function getHolders() public view returns (uint256){ + return holders.length; +} /** * Implements an additional lock on minting and burning, ensuring that supply changes happen after any potential distributions */
Hardhat
Check that the amount
is not zero in _beforeTransferToken
before adding the to
to holders
.
DoS
#0 - c4-pre-sort
2024-02-21T04:00:08Z
0xRobocop marked the issue as duplicate of #77
#1 - c4-judge
2024-03-04T13:15:35Z
0xA5DF marked the issue as satisfactory
🌟 Selected for report: BowTiedOriole
Also found by: 0x0bserver, 0xAadi, 0xJoyBoy03, 0xlamide, 0xlemon, 0xpiken, Babylen, Breeje, Brenzee, CodeWasp, DanielArmstrong, DarkTower, Fassi_Security, Fitro, Honour, JohnSmith, Krace, MrPotatoMagic, Myrault, ReadyPlayer2, SovaSlava, SpicyMeatball, TheSavageTeddy, Tigerfrake, atoko, cryptphi, csanuragjain, d3e4, gesha17, kinda_very_good, krikolkk, matejdb, max10afternoon, miaowu, n0kto, nuthan2x, parlayan_yildizlar_takimi, peanuts, petro_1912, pontifex, psb01, pynschon, rouhsamad, shaflow2, slippopz, spark, turvy_fuzz, web3pwn, zhaojohnson
7.1828 USDC - $7.18
https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L127-L146 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L164-L179 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L198-L237
The _beforeTokenTransfer
function does not verify whether the address of to
is equal to zero when adding new holders, thereby enabling any user to add a holder of address zero to holders
by burning tokens.
By adding holders, anyone can increase the length of holders
, potentially resulting in a denial-of-service (DoS) situation due to Out Of Gas, as both _afterTokenTransfer
and distribute
may iterate through the entire holders
.
In the _beforeTokenTransfer
function, the logic adds to
to holders
if it is a valid holder with a balance equal to zero before the transfer. During token burning, to
is set to zero, which bypasses the isApprovedHolder(to)
check. The exists
check only examines the balance of to
but not its address. Since the balance of ZERO is always zero, to
will consistently be added to holders
when tokens are burned.
function _beforeTokenTransfer( address from, address to, uint256 amount ) internal virtual override { require(!LockedForDistribution, "distribution in progress"); //@audit when burn, `to` is zero, which will not check the approval. if (!(to == address(0))) { require( isApprovedHolder(to), "receiver not approved to hold the token" ); } if (from == address(0) || to == address(0)) { _beforeMintOrBurn(); } //@audit `to` is added to holders even if `to` is zero. bool exists = (this.balanceOf(to) != 0); if (!exists) { holders.push(to); } }
This vulnerability allows anyone to exploit the situation and add elements to holders
, potentially causing a Denial-of-Service (DoS) situation due to Out of Gas, as both _afterTokenTransfer
and distribute
iterate through the entire holders
.
Attackers don't even need to be approved holders, they can simply call burn(0)
to add new holders.
function _afterTokenTransfer( address from, address to, uint256 amount ) internal virtual override { bool stillHolding = (this.balanceOf(from) != 0); if (!stillHolding) { for (uint i = 0; i < holders.length; i++) { if (holders[i] == from) { // Remove the element at i by copying the last one into its place and removing the last element holders[i] = holders[holders.length - 1]; holders.pop(); } } } }
function distribute(uint256 numDistributions) public nonReentrant { require(numDistributions > 0, "must process at least 1 distribution"); if (!LockedForDistribution) { require( _isPastMinDistributionPeriod(), "MinDistributionPeriod not met" ); _beginDistribution(); } uint256 limit = Math.min( nextDistributionRecipient + numDistributions, holders.length ); uint i; for (i = nextDistributionRecipient; i < limit; i++) { address recipient = holders[i]; if (isApprovedHolder(recipient)) { uint256[] memory receipts = new uint256[]( distributableERC20s.length ); for (uint j = 0; j < distributableERC20s.length; j++) { IERC20 toDistribute = IERC20(distributableERC20s[j]); uint256 entitlement = erc20EntitlementPerUnit[j] * this.balanceOf(recipient); if (toDistribute.transfer(recipient, entitlement)) { receipts[j] = entitlement; } } emit Distribution(recipient, distributableERC20s, receipts); } } nextDistributionRecipient = i; if (nextDistributionRecipient == holders.length) { _endDistribution(); } }
To validate the aforementioned issue, I conducted a test where holder1
represents an unauthorized holder. In this scenario, holder1
is able to continually add new holders (Address ZERO) to the holders
list by repeatedly burn zero tokens.
Apply the patch and run it with npx hardhat test
.
diff --git a/liquid-infrastructure/test/liquidERC20.ts b/liquid-infrastructure/test/liquidERC20.ts index 0121500..5d948b5 100644 --- a/liquid-infrastructure/test/liquidERC20.ts +++ b/liquid-infrastructure/test/liquidERC20.ts @@ -176,6 +176,27 @@ async function failToManageNFTNotOwner( ); } + +async function BurnToAddHolders( + infraERC20: LiquidInfrastructureERC20, + holder1: HardhatEthersSigner, + holder2: HardhatEthersSigner, + badSigner: HardhatEthersSigner +) { + + // Holder1 is not a valid holder + const infraERC20Holder1 = infraERC20.connect(holder1); + + var x = await infraERC20.getHolders(); + console.log("holders length ",x); + + await infraERC20Holder1.burn(0); + x = await infraERC20.getHolders(); + console.log("holders length ",x); + await infraERC20Holder1.burn(0); + x = await infraERC20.getHolders(); + console.log("holders length ",x); +} // Checks that only owner-approved holders are allowed to hold the ERC20, // and that even the owner cannot give them tokens without approving them async function basicErc20HolderTests( @@ -529,6 +550,13 @@ describe("LiquidInfrastructureERC20 tests", function () { await basicErc20HolderTests(infraERC20, holder1, holder2, badSigner); }); + it.only("burn to add holders", async function () { + const { infraERC20, holder1, holder2, badSigner } = + await liquidErc20Fixture(); + + await BurnToAddHolders(infraERC20, holder1, holder2, badSigner); + }); + it("manages distributions (basic)", async function () { const { infraERC20,
This diff add a view function to LiquidInfrastructureERC20.sol
to get the length of holders
.
diff --git a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol index 4722279..834cf4e 100644 --- a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol +++ b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol @@ -145,6 +145,9 @@ contract LiquidInfrastructureERC20 is } } +function getHolders() public view returns (uint256){ + return holders.length; +} /** * Implements an additional lock on minting and burning, ensuring that supply changes happen after any potential distributions */
Result:
LiquidInfrastructureERC20 tests holders length 0n holders length 1n holders length 2n ✔ burn to add holders (650ms) 1 passing (653ms)
Hardhat
Check that the to
is not a zero address in function _beforeTransferToken
before adding the to
to holders
.
DoS
#0 - c4-pre-sort
2024-02-21T04:00:21Z
0xRobocop marked the issue as duplicate of #77
#1 - c4-judge
2024-03-04T13:15:50Z
0xA5DF marked the issue as satisfactory
🌟 Selected for report: nuthan2x
Also found by: 0x0bserver, AM, CaeraDenoir, DanielArmstrong, JrNet, Kirkeelee, KmanOfficial, Krace, Limbooo, Meera, SovaSlava, SpicyMeatball, TheSavageTeddy, agadzhalov, aslanbek, atoko, csanuragjain, d3e4, imare, jesjupyter, juancito, kartik_giri_47538, kutugu, max10afternoon, offside0011, pkqs90, turvy_fuzz, xchen1130, zhaojohnson, ziyou-
25.7286 USDC - $25.73
https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L198-L237 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L257-L281 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L441-L445
If the owner changes distributableERC20s
during the distribute
process, certain holders may receive an incorrect amount of tokens.
distribute
calls _beginDistribution
to calculate the erc20EntitlementPerUnit
if the process of distribution has just begun. And it will distribute the erc20 to the holders according to the distributableERC20s
and the calculated erc20EntitlementPerUnit
.
function distribute(uint256 numDistributions) public nonReentrant { require(numDistributions > 0, "must process at least 1 distribution"); //@audit If the distribute is just begin, calculate the Unit. if (!LockedForDistribution) { require( _isPastMinDistributionPeriod(), "MinDistributionPeriod not met" ); _beginDistribution(); } uint256 limit = Math.min( nextDistributionRecipient + numDistributions, holders.length ); uint i; for (i = nextDistributionRecipient; i < limit; i++) { address recipient = holders[i]; if (isApprovedHolder(recipient)) { uint256[] memory receipts = new uint256[]( distributableERC20s.length ); //@audit distribute the erc20 according to the distributableERC20s and Unit for (uint j = 0; j < distributableERC20s.length; j++) { IERC20 toDistribute = IERC20(distributableERC20s[j]); uint256 entitlement = erc20EntitlementPerUnit[j] * this.balanceOf(recipient); if (toDistribute.transfer(recipient, entitlement)) { receipts[j] = entitlement; } } emit Distribution(recipient, distributableERC20s, receipts); } } nextDistributionRecipient = i; if (nextDistributionRecipient == holders.length) { _endDistribution(); } }
function _beginDistribution() internal { require( !LockedForDistribution, "cannot begin distribution when already locked" ); LockedForDistribution = true; // clear the previous entitlements, if any if (erc20EntitlementPerUnit.length > 0) { delete erc20EntitlementPerUnit; } // Calculate the entitlement per token held uint256 supply = this.totalSupply(); for (uint i = 0; i < distributableERC20s.length; i++) { uint256 balance = IERC20(distributableERC20s[i]).balanceOf( address(this) ); uint256 entitlement = balance / supply; erc20EntitlementPerUnit.push(entitlement); } nextDistributionRecipient = 0; emit DistributionStarted(); }
However, the owner could change the distributableERC20s
even if the distribute
is not complete.
function setDistributableERC20s( address[] memory _distributableERC20s ) public onlyOwner { distributableERC20s = _distributableERC20s; }
Considering the following scenarios:
distributableERC20s
: [USDC, USDT, DAI], holders
: [1,2,3] with same balances of LiquidInfrastructureERC20
.distribute(1)
calls _beginDistribution
to initialize the erc20EntitlementPerUnit
to [10, 20, 30] and transfers corresponding balances to holders1 ==> [10 USDC, 20 USDT, 30 DAI]setDistributableERC20s
sets the distributableERC20s
to [DAI, USDT, USDC]. The erc20EntitlementPerUnit
is still [10, 20, 30].distribute(2)
will transfer incorrect value of tokens to holder2 and holder3 because the order of distributableERC20s
has been changed ==> [10 DAI, 20 USDT, 30 USDC].Manual Review
Disallow the execution of setDistributableERC20s
during the ongoing process of distribute
until it is completed.
diff --git a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol index 4722279..6c86579 100644 --- a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol +++ b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol @@ -441,6 +441,7 @@ contract LiquidInfrastructureERC20 is function setDistributableERC20s( address[] memory _distributableERC20s ) public onlyOwner { + require(!LockedForDistribution, "distribution in progress"); distributableERC20s = _distributableERC20s; }
Context
#0 - c4-pre-sort
2024-02-22T06:08:47Z
0xRobocop marked the issue as duplicate of #260
#1 - c4-judge
2024-03-04T15:32:16Z
0xA5DF marked the issue as satisfactory
#2 - c4-judge
2024-03-08T15:13:03Z
0xA5DF changed the severity to 3 (High Risk)
#3 - c4-judge
2024-03-08T15:26:19Z
0xA5DF changed the severity to 2 (Med Risk)
🌟 Selected for report: SpicyMeatball
Also found by: BowTiedOriole, Breeje, CaeraDenoir, JohnSmith, Krace, Meera, PumpkingWok, SovaSlava, SpicyMeatball, d3e4, juancito, kutugu, nuthan2x, rokinot, rouhsamad, web3pwn
242.1143 USDC - $242.11
https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L413-L434 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L359-L386
If the owner uses releaseManagedNFT
during the withdrawFromManagedNFTs
process, certain ManagedNFTs may be unable to withdraw their balances to this contract. This action could potentially result in a denial-of-service (DoS) scenario for the withdrawFromManagedNFTs
function, especially if the length of the new ManagedNFTs
is less than the current nextWithdrawal
.
withdrawFromManagedNFTs
iterates through the ManagedNFTs
and withdraws balances accordingly. It utilizes nextWithdrawal
to keep track of the index of the next NFT's ID for withdrawal.
function withdrawFromManagedNFTs(uint256 numWithdrawals) public { require(!LockedForDistribution, "cannot withdraw during distribution"); if (nextWithdrawal == 0) { emit WithdrawalStarted(); } uint256 limit = Math.min( numWithdrawals + nextWithdrawal, ManagedNFTs.length ); uint256 i; for (i = nextWithdrawal; i < limit; i++) { LiquidInfrastructureNFT withdrawFrom = LiquidInfrastructureNFT( ManagedNFTs[i] ); (address[] memory withdrawERC20s, ) = withdrawFrom.getThresholds(); withdrawFrom.withdrawBalancesTo(withdrawERC20s, address(this)); emit Withdrawal(address(withdrawFrom)); } nextWithdrawal = i; if (nextWithdrawal == ManagedNFTs.length) { nextWithdrawal = 0; emit WithdrawalFinished(); } }
However, releaseManagedNFT
has the capability to remove the ManagedNFTs
at any time, even if the process of withdrawFromManagedNFTs
is not finished (i.e., when nextWithdrawal
is not zero). This could potentially lead to unexpected situations.
function releaseManagedNFT( address nftContract, address to ) public onlyOwner nonReentrant { LiquidInfrastructureNFT nft = LiquidInfrastructureNFT(nftContract); nft.transferFrom(address(this), to, nft.AccountId()); // Remove the released NFT from the collection for (uint i = 0; i < ManagedNFTs.length; i++) { address managed = ManagedNFTs[i]; if (managed == nftContract) { // Delete by copying in the last element and then pop the end ManagedNFTs[i] = ManagedNFTs[ManagedNFTs.length - 1]; ManagedNFTs.pop(); break; } } // By this point the NFT should have been found and removed from ManagedNFTs require(true, "unable to find released NFT in ManagedNFTs"); emit ReleaseManagedNFT(nftContract, to); }
Considering two scenarios:
Scenario 1:
withdrawFromManagedNFTs(2)
withdraws balances from 1 and 2, setting nextWithdrawal to 2.releaseManagedNFT(2, anyaddress)
removes 2 from ManagedNFTs, resulting in ManagedNFTs=[1,3,4].withdrawFromManagedNFTs(2)
withdraws balances from 4 because the element at index 2 in ManagedNFTs is 4.Scenario 2:
withdrawFromManagedNFTs(2)
withdraws balances from 1, 2, and 3, setting nextWithdrawal to 3.releaseManagedNFT(1, anyaddress)
removes 1 from ManagedNFTs, resulting in ManagedNFTs=[2,3,4].releaseManagedNFT(2, anyaddress)
removes 2 from ManagedNFTs, resulting in ManagedNFTs=[3,4].withdrawFromManagedNFTs(2)
cannot withdraw new balances because nextWithdrawal = 3
is greater than the length of ManagedNFTs
.Manual Review
Disallow the execution of releaseManagedNFT
during the ongoing process of withdrawFromManagedNFTs
until it is completed.
Context
#0 - c4-pre-sort
2024-02-21T04:42:04Z
0xRobocop marked the issue as duplicate of #130
#1 - c4-judge
2024-03-03T12:59:10Z
0xA5DF marked the issue as satisfactory