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: 26/84
Findings: 2
Award: $127.63
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: ZanyBonzy
Also found by: 0xSmartContractSamurai, DarkTower, MrPotatoMagic, SpicyMeatball, TheSavageTeddy, jesjupyter, lsaudit, peanuts, zhaojohnson
11.348 USDC - $11.35
LiquidInfrastructureERC20::constructor()
- Although OZ version v4.3.1 should be sufficient for this deployment version, recommended to use the latest version of OZ, v5.0.0, which initializes the contract setting the address provided by the deployer as the initial owner.https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L408 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L5
@openzeppelin/contracts/access/Ownable.sol
:
constructor(address initialOwner) {
Example implementation:
LiquidInfrastructureERC20:constructor()
:
constructor( string memory _name, string memory _symbol, address[] memory _managedNFTs, address[] memory _approvedHolders, uint256 _minDistributionPeriod, address[] memory _distributableErc20s, + address _initialOwner ) ERC20(_name, _symbol) Ownable(_initialOwner) {
Dev comment may be somewhat confusing/unclear:
"Minting and burning of this ERC20 is restricted if the minimum distribution period has elapsed, and it is reenabled once a new distribution is complete."
The above dev comment implies that minting/burning is allowed during the minimum distribution period. But then the confusing comment of "it is reenabled once a new distribution is complete."
Some questions:
indexed
in event declarations.https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L30-L38 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol#L35-L38
Taking as examples:
event SuccessfulWithdrawal(address to, address[] erc20s, uint256[] amounts);
event Distribution(address recipient, address[] tokens, uint256[] amounts);
The indexed
keyword in Solidity event declarations allows for efficient filtering and searching of events, leading to lower gas costs and improved search efficiency, especially for frequently occurring events or when optimizing gas usage. Marking parameters like to
as indexed
in events such as SuccessfulWithdrawal
can improve search efficiency for targeted log retrieval based on specific criteria like the to
address.
LiquidInfrastructureERC20::MinDistributionPeriod()
- the code implementation at L220 does not reflect the natspec comment for this state variable.Take note of the "required to elapse before"
in below natspec comment, specifically the "elapse"
word. It clearly means AFTER. I.e. new distribution can only begin AFTER the MinDistributionPeriod
has ELAPSED, but the code implementation allows for new distribution period to begin AT/during the last block of the MinDistributionPeriod
, which is an incorrect implementation. This implementation bug is covered in a different bug report. This QA/LOW report is regarding the inconsistency between the state variable declaration's natspec comment and how it is implemented/used in the codebase.
/** * @notice Holds the minimum number of blocks required to elapse before a new distribution can begin */ uint256 public MinDistributionPeriod;
And the implementation at L220:
return (block.number - LastDistribution) >= MinDistributionPeriod;
I cover this incorrect implementation in a separate bug report, either medium or high.
public
modifier is used with no internal calls to the function, therefore the external
visibility modifier should be used, in line with standard recommendations & best practices.https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L100 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L110 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L267 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L289 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L302 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L309 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L347 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L363 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L388 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol#L85 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol#L109 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol#L136 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol#L204
The affected functions are:
approveHolder()
,
disapproveHolder()
,
mintAndDistribute()
,
burnAndDistribute()
,
burnFromAndDistribute()
,
withdrawFromAllManagedNFTs()
,
addManagedNFT()
,
releaseManagedNFT()
,
setDistributableERC20s()
,
getThresholds()
,
setThresholds()
,
withdrawBalances()
,
recoverAccount()
LiquidInfrastructureERC20::approveHolder()
- Owner can make himself or this contract a holder.Although owner can easily just add any one of his own addresses thereby becoming a holder, at the very least he should not be allowed to make his own msg.sender
address or address(this)
a holder, to help maintain trust in the protocol.
function approveHolder(address holder) public onlyOwner { require(!isApprovedHolder(holder), "holder already approved"); HolderAllowlist[holder] = true; }
LiquidInfrastructureERC20::_beforeTokenTransfer()
, LiquidInfrastructureERC20::_beforeMintOrBurn()
and LiquidInfrastructureERC20::_afterTokenTransfer()
.Consider commenting out or removing these three unused functions unless there's a plan to use them in future upgrades. They will add to deployment gas cost overhead in a contract that already contains functions that are gas guzzlers.👀
holder
address can be added to the holders
array more than once, as long as the holder's balance is zeroed out/emptied before calling _beforeTokenTransfer()
. Can effectively receive 2x or more rewards.LOW severity because the affected internal functions are NOT in use, otherwise could have been medium or high severity depending.
_beforeTokenTransfer()
& _afterTokenTransfer()
are replaced by _beginDistribution()
& _endDistribution()
respectively?https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L121 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L148 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L229 https://github.com/code-423n4/2024-02-althea-liquid-infrastructure/blob/3adc34600561077ad4834ee9621060afd9026f06/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol#L253
LiquidInfrastructureERC20::distributeToAllHolders()
- Should handle the case where holders.length = 0
.Recommendation:
function distributeToAllHolders() public { uint256 num = holders.length; if (num > 0) { distribute(holders.length); + } else { + revert HoldersArrayEmpty(); + } }
Or if dont want to revert, add a suitable event instead.
withdrawFromManagedNFTs()
.function withdrawFromManagedNFTs(uint256 numWithdrawals) public {
Impact:
Recommendation:
releaseManagedNFT()
should use safeTransferFrom()
instead.nft.transferFrom(address(this), to, nft.AccountId());
Recommendation:
Use OZ's safeTransferFrom()
.
LiquidInfrastructureERC20::constructor()
- Since MinDistributionPeriod
cannot be updated after deployment, unless upgrade, should have check to ensure value != 0.+ require(_minDistributionPeriod != 0, "_minDistributionPeriod cannot be zero"); MinDistributionPeriod = _minDistributionPeriod;
LiquidInfrastructureNFT::constructor()
- Unless there's a valid reason, should definitely use _safeMint
instead."Usage of this method is discouraged, use {_safeMint} whenever possible".
_mint(msg.sender, AccountId);
#0 - c4-pre-sort
2024-02-22T17:02:03Z
0xRobocop marked the issue as sufficient quality report
#1 - 0xA5DF
2024-03-08T12:01:46Z
+L from #747
#2 - 0xA5DF
2024-03-08T12:19:20Z
1L from HMs
R NC NC NC NC R F L NC F F R R Bot report
= 2L 4R 5NC
#3 - c4-judge
2024-03-08T14:27:30Z
0xA5DF marked the issue as grade-c
#4 - c4-judge
2024-03-09T08:05:02Z
0xA5DF marked the issue as grade-b
🌟 Selected for report: 0x11singh99
Also found by: 0xSmartContractSamurai, c3phas, dharma09
116.2791 USDC - $116.28
LiquidInfrastructureERC20::distributeToAllHolders()
- L167: Could contribute to out of gas DoS due to use of state variable holders.length
as parameter for calling method distribute()
. Should use cached local variable num
instead.The code change from distribute(holders.length
) to distribute(num)
in the distributeToAllHolders()
function improves gas efficiency by using a locally cached variable num
to store the value of holders.length
and then passing num
as a parameter to the distribute()
function. This optimization reduces gas costs by avoiding redundant computation and storage access:
/** * Performs a distribution to all of the current holders, which may trigger out of gas errors if there are too many holders */ function distributeToAllHolders() public { uint256 num = holders.length; if (num > 0) { - distribute(holders.length); + distribute(num); } }
LiquidInfrastructureERC20::
- Use != 0
instead of > 0
, to save some gas.- if (num > 0) { + if (num != 0) {
- require(numDistributions > 0, "must process at least 1 distribution"); + require(numDistributions != 0, "must process at least 1 distribution");
LiquidInfrastructureERC20::distribute()
- some much needed gas optimizations for this function.Recommended gas optimizations:
function distribute(uint256 numDistributions) public nonReentrant { - require(numDistributions > 0, "must process at least 1 distribution"); + require(numDistributions != 0, "must process at least 1 distribution"); if (!LockedForDistribution) { require(_isPastMinDistributionPeriod(), "MinDistributionPeriod not met"); _beginDistribution(); } + /// cache some state variables + uint256 _nextDistributionRecipient = nextDistributionRecipient; + uint256 _holdersLength = holders.length; - uint256 limit = Math.min(nextDistributionRecipient + numDistributions, holders.length); + uint256 limit = Math.min(_nextDistributionRecipient + numDistributions, _holdersLength); + uint256 distributableERC20sLength = distributableERC20s.length; uint256 i; - for (i = nextDistributionRecipient; i < limit; i++) { + for (i = _nextDistributionRecipient; i < limit; i++) { address recipient = holders[i]; if (isApprovedHolder(recipient)) { - uint256[] memory receipts = new uint256[](distributableERC20s.length); + uint256[] memory receipts = new uint256[](distributableERC20sLength); + uint256 thisBalanceOfRecipient = this.balanceOf(recipient); - for (uint256 j = 0; j < distributableERC20s.length; j++) { + for (uint256 j; j < distributableERC20sLength; j++) { IERC20 toDistribute = IERC20(distributableERC20s[j]); - uint256 entitlement = erc20EntitlementPerUnit[j] * this.balanceOf(recipient); + uint256 entitlement = erc20EntitlementPerUnit[j] * thisBalanceOfRecipient; if (toDistribute.transfer(recipient, entitlement)) { receipts[j] = entitlement; } } emit Distribution(recipient, distributableERC20s, receipts); } } nextDistributionRecipient = i; + _nextDistributionRecipient = nextDistributionRecipient; - if (nextDistributionRecipient == holders.length) { + if (_nextDistributionRecipient == _holdersLength) { _endDistribution(); } }
Cache the state variable's length value:
+ uint256 distributableERC20sLength = distributableERC20s.length; - for (uint256 i = 0; i < distributableERC20s.length; i++) { + for (uint256 i; i < distributableERC20sLength; i++) {
withdrawFromAllManagedNFTs()
- Cache the state variable and use the cached local variable as the method's parameter.function withdrawFromAllManagedNFTs() public { + uint256 ManagedNFTsLength = ManagedNFTs.length; - withdrawFromManagedNFTs(ManagedNFTs.length); + withdrawFromManagedNFTs(ManagedNFTsLength); }
withdrawFromManagedNFTs()
.function withdrawFromManagedNFTs(uint256 numWithdrawals) public { require(!LockedForDistribution, "cannot withdraw during distribution"); + uint256 _nextWithdrawal = nextWithdrawal; - if (nextWithdrawal == 0) { + if (_nextWithdrawal == 0) { emit WithdrawalStarted(); } + uint256 ManagedNFTsLength = ManagedNFTs.length; - uint256 limit = Math.min(numWithdrawals + nextWithdrawal, ManagedNFTs.length); + uint256 limit = Math.min(numWithdrawals + _nextWithdrawal, ManagedNFTsLength); uint256 i; - for (i = nextWithdrawal; i < limit; 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; + _nextWithdrawal = nextWithdrawal; - if (nextWithdrawal == ManagedNFTs.length) { + if (_nextWithdrawal == ManagedNFTsLength) { nextWithdrawal = 0; emit WithdrawalFinished(); } }
ManagedNFTs.length
.function releaseManagedNFT(address nftContract, address to) public onlyOwner nonReentrant { LiquidInfrastructureNFT nft = LiquidInfrastructureNFT(nftContract); nft.transferFrom(address(this), to, nft.AccountId()); + uint256 ManagedNFTsLength = ManagedNFTs.length; // Remove the released NFT from the collection - for (uint256 i = 0; i < ManagedNFTs.length; i++) { + for (uint256 i; i < ManagedNFTsLength; 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[i] = ManagedNFTs[ManagedNFTsLength - 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);
#0 - c4-pre-sort
2024-02-22T18:06:49Z
0xRobocop marked the issue as sufficient quality report
#1 - c4-judge
2024-03-08T14:36:01Z
0xA5DF marked the issue as grade-c
#2 - c4-judge
2024-03-08T16:10:53Z
0xA5DF marked the issue as grade-a
#3 - 0xA5DF
2024-03-08T16:11:29Z
G2 is saving here a significant amount of gas, esp. with caching this.balanceOf(recipient)