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: 4/84
Findings: 2
Award: $864.44
π 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#L164 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L142 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L144
There is a potential DoS attack in the _afterTokenTransfer
function which will make the protocol break and the owner can't mint tokens again.
As we know
"When a function call gets a gas error The entire transaction, including any changes made by previous function calls within the transaction, is reverted. This means that all state changes made up to that point are undone"
in this snippet code in the _beforeTokenTransfer
function:
//existing codes... // @note: Alice and users that Alice approved can burn Alice's tokens to push address(0) to `holders` @> bool exists = (this.balanceOf(to) != 0); // @audit-High : add this condition to prevent DOS attack in _afterTokenTransfer : if(!exists && to != address(0)) if (!exists) { holders.push(to); }
every time a user wants to burn its tokens, the zero address will be pushed to holders because the exists
becomes false because the balance of zero address for the token is 0 thanks to the _burn
function which decreased the balance of the from
but not increased the balance of zero address.
Here is the step-by-step scenario of how a malicious user can make a DoS attack:
after the deployment, the owner mints tokens for some users (for example 10,000).
a malicious one burns its tokens one by one, unit by unit to push more zero addresses to the holders array. He can also do that with an approved malicious contract by the user to keep calling the burnFrom(_maliciousUser, 1)
function. this is possible because the balance of zero address is always will be 0 and it will enter the if statement:
// `exists` is equal to false if(!exists){ holders.push(to); }
we might see 10,000 zero addresses in holders that aren't the actual holders. however, lots of users might burn their tokens, and for everyone who burns tokens, a zero address will always be pushed so 10,000 is just a small number.
in the _afterTokenTransfer
function :
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(); } } } }
of course, the goal of the function is that every holder that leads its balance to zero of this token contract will be removed from the holders
array.
when the owner the next time after the deployment wants to mint tokens again via the mint
function, the whole process of minting will be reverted due to the gas error. because:
//ERC20.sol function _mint(address account, uint256 amount) internal virtual { require(account != address(0), "ERC20: mint to the zero address"); _beforeTokenTransfer(address(0), account, amount); _totalSupply += amount; _balances[account] += amount; emit Transfer(address(0), account, amount); @> _afterTokenTransfer(address(0), account, amount); }
The _mint
function will call the _afterTokenTransfer
and pass the zero address as from
then from
will enter the loop in _afterTokenTransfer
and pop up every zero address that exists in holders
but due to the large number of them,
it'll get reverted...
Manual Review
Don't let the zero address be pushed to the holders
array.
//existing codes... if (!exists && to != address(0)) { holders.push(to); }
DoS
#0 - c4-pre-sort
2024-02-20T03:15:53Z
0xRobocop marked the issue as primary issue
#1 - c4-pre-sort
2024-02-20T03:15:57Z
0xRobocop marked the issue as high quality report
#2 - 0xRobocop
2024-02-20T03:20:24Z
Related to #729 in the sense that both identify that the _afterTokenTransfer
may get DoSed by a long holder's array.
But this issue clearly gives the steps on how an attacker can make the array to grow indefinitely.
#3 - c4-pre-sort
2024-02-20T06:34:13Z
0xRobocop marked the issue as duplicate of #77
#4 - c4-pre-sort
2024-02-22T15:05:54Z
0xRobocop marked the issue as remove high or low quality report
#5 - c4-judge
2024-03-03T13:30:00Z
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
Judge has assessed an item in Issue #575 as 3 risk. The relevant finding follows:
Summary: everytime the approved users burn their tokens, the zero address will be pushed to the holders array.
Detail:
inside the _beforeTokenTransfer
function:
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(); } // @note: Alice and users that Alice approved can burn Alice's tokens to push address(0) to `holders` bool exists = (this.balanceOf(to) != 0); if (!exists) { holders.push(to); } }
as you see if approved users burn their tokens, the zero address will be pushed to the holders
array cause the zero address's balance is always 0.
this is because, in the ERC20.sol in the _burn
function, the function reduces the balance of the from
but doesn't increase the balance of the zero address.
so It will cause to exists
variable to always be true if to
is a zero address.
Impact: Holders should be actual holders not a bunch of zero addresses. this is just breaking the functionality of the protocol.
Recommendation:
add another condition in the if statement to don't push zero addresses:
//existing codes... + if(!exists && to != address(0)) //existing codes...
#0 - c4-judge
2024-03-04T13:25:22Z
0xA5DF marked the issue as duplicate of #77
#1 - c4-judge
2024-03-04T13:25:27Z
0xA5DF marked the issue as partial-50
#2 - 0xA5DF
2024-03-04T13:26:05Z
Didn't fully identify the impact
π Selected for report: Fassi_Security
Also found by: 0xJoyBoy03, max10afternoon, web3pwn
857.2555 USDC - $857.26
https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L359 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L184 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/bd6ee47162368e1999a0a5b8b17b701347cf9a7d/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L413
There is a scenario in which a Malicious user can delay the revenue for another MinDistributionPeriod
.
Holders earn revenue from withdrawERC20s
. The protocol obliged to aggregate all the withdrawERC20s
tokens of all ManagedNFTs
into LiquidInfrastructureERC20
and send them to holders.
but a Malicious user can delay some of the lists of withdrawERC20s
to another MinDistributionPeriod
.
This will lead to more unhappy customers and a weak protocol because the Attacker can freeze the revenues of holders for another MinDistributionPeriod
.
Suppose we have these assumptions:
MinDistributionPeriod
= 30 days (assume X
in block number)ManagedNFTs.length
= 5withdrawERC20s
(or distributableERC20s
) = [Dai, USDT, USDC]Here is the step-by-step scenario:
A malicious user can call the withdrawFromManagedNFTs(2)
function in the X - 1
block number. the result is that all of the withdrawERC20s
tokens in the first two ManagedNFTs
are sent to the LiquidInfrastructureERC20
and it will charged. note that the nextWithdrawal
variable is equal to 2.
After that, he will call the distributeToAllHolders()
function to distribute the first two assets of ManagedNFTs
to holders and end the distribution.
this will lead to setting LastDitribution
to the current block number and it'll kind of reset the MinDistributionPeriod
Holders must wait for another MinDistributionPeriod
(30 days) to receive their shares from other assets of ManagedNFTs
. their assets will freeze for another 30 days.
Griefing is Done!!!
@Note: This scenario can be worse if the owner pops up one of the ManagedNFTs
with the releaseManagedNFT
function. in our example, if the owner unintentionally pops up the first or second ManagedNFTs
array, based on the protocol, the last member of the ManagedNFTs
will replaced with the first or second member. then recalling the withdrawFromManagedNFTs
function will query the ManagedNFTs
starting from 2 cause the nextWithdrawal
is 2 and the replaced member of ManagedNFTs
should wait for another time.
Manual Review
Make the withdrawFromManagedNFTs
function internal to call it just via the withdrawFromAllManagedNFTs
function or Add an onlyOwner
modifier.
+ function withdrawFromManagedNFTs(uint256 numWithdrawals) public onlyOwner { //existing codes... }
Access Control
#0 - c4-pre-sort
2024-02-21T05:27:53Z
0xRobocop marked the issue as sufficient quality report
#1 - c4-pre-sort
2024-02-21T05:27:56Z
0xRobocop marked the issue as primary issue
#2 - c4-sponsor
2024-03-02T03:56:13Z
ChristianBorst (sponsor) acknowledged
#3 - ChristianBorst
2024-03-02T04:01:09Z
I think this is a valid suggestion but does not pose a risk to the protocol. This could enable a malicious owner (which is already a highly trusted role) to avoid distributing revenue to holders before minting new tokens.
There is no restriction on distribution frequency, a user could call distributeToAllHolders() multiple times every single block without withdrawing rewards from the ManagedNFTs if they want to pay the gas to do so. Even if a user decides to pay all that gas, nothing is preventing any other accounts from withdrawing rewards from the ManagedNFTs and performing yet another distribution to actually distribute the revenue to the holders.
I think that there is an argument to restrict owner misbehavior by forcing withdrawal before distribution, but the recommended mitigation strategy would introduce a new DoS vector to the protocol. Therefore I think the severity should be lower.
#4 - c4-sponsor
2024-03-02T04:01:12Z
ChristianBorst marked the issue as disagree with severity
#5 - 0xA5DF
2024-03-04T12:08:47Z
There is no restriction on distribution frequency, a user could call distributeToAllHolders() multiple times every single block
There is actually a restriction, you have to wait MinDistributionPeriod
before calling it again (code)
See dupe #119, it's more clear and the mitigation makes more sense (ensuring withdrawFromManagedNFTs()
was completed before starting distribution).
Given that the min period is going to be about a week or a month I think this is a valid med issue.
#6 - c4-judge
2024-03-04T12:09:08Z
0xA5DF marked issue #119 as primary and marked this issue as a duplicate of 119
#7 - c4-judge
2024-03-04T12:09:13Z
0xA5DF marked the issue as satisfactory
#8 - c4-judge
2024-03-09T11:27:14Z
0xA5DF marked the issue as partial-25