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: 25/84
Findings: 1
Award: $151.16
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: 0x11singh99
Also found by: 0xSmartContractSamurai, c3phas, dharma09
151.1628 USDC - $151.16
Note : G-02 and G-04 contains only those instances which were missed by bot.
erc20EntitlementPerUnit
and distributableERC20s
storage arrays in memory outside of nested for loops to avoid multiple Gwarmsload
for every i
value.Since two for
loops are nested here. So internal for loop will run distributableERC20s.length
times for every i
value. That means every element in distributableERC20s
and distributableERC20s.length
elements in erc20EntitlementPerUnit
array will be accessed for limit - nextDistributionRecipient
times ie. for each i
value.
By caching those arrays outside of both loops can result in accessing those array's distributableERC20s.length
no. of starting elements only one times instead of multiple times.
It can save total 2 * limit - nextDistributionRecipient - 1
* distributableERC20s.length
SLOAD
. Which can save ~100 Gas (( 97 Gas technically)) on avoiding each SLOAD when accessed after first time . Since each SLOAD
cost 100 Gas while each MLOAD takes only 3 Gas.
limit - nextDistributionRecipient - 1
* distributableERC20s.length
* 97 Gas will be saved.File : contracts/LiquidInfrastructureERC20.sol 214: for (i = nextDistributionRecipient; i < limit; i++) { 215: address recipient = holders[i]; 216: if (isApprovedHolder(recipient)) { 217: uint256[] memory receipts = new uint256[]( 218: distributableERC20s.length 219: ); 220: for (uint j = 0; j < distributableERC20s.length; j++) { 221: IERC20 toDistribute = IERC20(distributableERC20s[j]); //@audit cache `erc20EntitlementPerUnit` and `distributableERC20s` outside from here and access here from memory 222: uint256 entitlement = erc20EntitlementPerUnit[j] * 223: this.balanceOf(recipient); 224: if (toDistribute.transfer(recipient, entitlement)) { 225: receipts[j] = entitlement; 226: } 227: } 228: 229: emit Distribution(recipient, distributableERC20s, receipts); 230: } 231: }
Recommended Mitigation Steps:
+ uint256[] memory m_erc20EntitlementPerUnit = new uint256[](distributableERC20s.length); + address[] memory m_distributableERC20s = new address[](distributableERC20s.length); + m_erc20EntitlementPerUnit = erc20EntitlementPerUnit; + m_distributableERC20s = distributableERC20s; 214: for (i = nextDistributionRecipient; i < limit; i++) { 215: address recipient = holders[i]; 216: if (isApprovedHolder(recipient)) { 217: uint256[] memory receipts = new uint256[]( 218: distributableERC20s.length 219: ); 220: for (uint j = 0; j < distributableERC20s.length; j++) { - 221: IERC20 toDistribute = IERC20(distributableERC20s[j]); - 222: uint256 entitlement = erc20EntitlementPerUnit[j] * + 221: IERC20 toDistribute = IERC20(m_distributableERC20s[j]); + 222: uint256 entitlement = m_erc20EntitlementPerUnit[j] * 223: this.balanceOf(recipient); 224: if (toDistribute.transfer(recipient, entitlement)) { 225: receipts[j] = entitlement; 226: } 227: } 228: 229: emit Distribution(recipient, distributableERC20s, receipts); 230: } 231: }
LiquidInfrastructureERC20.sol#L214C1-L231C10
2000 GAS, 1 SLOT
If variables occupying the same slot are both written the same function or by the constructor, avoids a separate Gsset (20000 gas). Reads of the variables can also be cheaper when accessed in same transaction.
LastDistribution
and LockedForDistribution
can be packed in single
slot SAVES: 2000 GAS, 1 Storage SLOTs
LastDistribution
This can be reduced to uint48
since it holds block number so uint48 is more than sufficient to hold any realistic block number of any blockchain.
Note: It only include instance which was missed by bot. MinDistributionPeriod
already covered in bot report. So it doesn't included here. While LastDistribution
reducing size is not covered in bot report. And it is major gas saving so it is covered here.
File : contracts/LiquidInfrastructureERC20.sol 70: uint256 public LastDistribution; 75: uint256 public MinDistributionPeriod; 80: bool public LockedForDistribution;
LiquidInfrastructureERC20.sol#L70-L80
Recommended Mitigation Steps:
File : contracts/LiquidInfrastructureERC20.sol -70: uint256 public LastDistribution; 75: uint256 public MinDistributionPeriod; + uint48 public LastDistribution; 80: bool public LockedForDistribution;
nonReentrant
modifier checks in functions(saves ~24000 GAS)SAVES ~24000 GAS
when every time respective function calledRemove the unnecessary nonReentrant
modifier since this function does not contain any external calls, and it is not invoked within the _mint
function flow. This function mint
not called again in _mint
flow also not in overridden _beforeTokenTransfer
and _afterTokenTransfer
.So there is no chance of reentrancy in this context. The _mint
function, which is part of the OpenZeppelin library, includes hooks before and after token transfer, and they do not involve any external calls in their overridden functions.and _mint
already doesn't have any external call. Removing the nonReentrant
modifier results in a gas saving of approximately ~24700 gas.
File : contracts/LiquidInfrastructureERC20.sol 318: function mint( 319: address account, 320: uint256 amount 321: ) public onlyOwner nonReentrant { 322: _mint(account, amount); 323: }
LiquidInfrastructureERC20.sol#L318C4-L323C6
Recommended Mitigation Steps:
File : contracts/LiquidInfrastructureERC20.sol 318: function mint( 319: address account, 320: uint256 amount -321: ) public onlyOwner nonReentrant { +321: ) public onlyOwner { 322: _mint(account, amount); 323: }
cached
value instead of re-reading from storage
( instance Missed by bot)Since reading state var. from storage takes 100 Gas in other consecutive after first access. So to avoid 1 SLOAD we can use already cached value.
Note: It only include those instances which were missed by bot
File : liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol 184: function distributeToAllHolders() public { 185: uint256 num = holders.length; 186: if (num > 0) { 187: distribute(holders.length); 188: } 189: }
LiquidInfrastructureERC20.sol#L184C4-L189C6
Recommended Mitigation Steps:
File : liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol function distributeToAllHolders() public { uint256 num = holders.length; if (num > 0) { - distribute(holders.length); + distribute(num); } }
inherit
ERC721
againSince OwnableApprovableERC721
already inherits ERC721
so no need to inherit ERC721
again in LiquidInfrastructureNFT
contract.
20: abstract contract OwnableApprovableERC721 is Context, ERC721 {
There is no need to explicitly inherit ERC721
again in this context. Additionally removing the unnecessary import of ERC721
above will streamline the code.
File : contracts/LiquidInfrastructureNFT.sol 5: import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; ... 33: contract LiquidInfrastructureNFT is ERC721, OwnableApprovableERC721 {
LiquidInfrastructureNFT.sol#L5C1-L33C70 , OwnableApprovableERC721.sol#L20
Recommended Mitigation steps:
File : contracts/LiquidInfrastructureNFT.sol -5: import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; ... -33: contract LiquidInfrastructureNFT is ERC721, OwnableApprovableERC721 { +33: contract LiquidInfrastructureNFT is OwnableApprovableERC721 {
Remove
unnecessary require
statementsSaves ~115 Gas on every time _beginDistribution
function called.
Since _beginDistribution
function called only once inside distribute
when LockedForDistribution
is false.
So LockedForDistribution
will always be false when _beginDistribution
will be called. So there is no requirement of require
check in _beginDistribution
starting at line 258.
It can save 1 SLOAD (~100 Gas) + 1 NOT opcode(~3 gas) + 1 require check which will always be true(~15 Gas).
File : contracts/LiquidInfrastructureERC20.sol function distribute(uint256 numDistributions) public nonReentrant { 200: if (!LockedForDistribution) { 201: require( 202: _isPastMinDistributionPeriod(), 203: "MinDistributionPeriod not met" 204: ); 205: _beginDistribution(); 206: }
Remove the below require statement
File : contracts/LiquidInfrastructureERC20.sol 257: function _beginDistribution() internal { 258: require( 259: !LockedForDistribution, 260: "cannot begin distribution when already locked" 263: );//@audit gas remove this unnecessary require 264: LockedForDistribution = true;
LiquidInfrastructureERC20.sol#L200C1-L206C10 , LiquidInfrastructureERC20.sol#L257C5-L262C38
require
will never revert here so remove it. It doesn't have any utility here only wasting little gas in executing and always pass.
File : contracts/LiquidInfrastructureERC20.sol 431: require(true, "unable to find released NFT in ManagedNFTs");
LiquidInfrastructureERC20.sol#L431
File : contracts/LiquidInfrastructureERC20.sol - 431: require(true, "unable to find released NFT in ManagedNFTs");
#0 - c4-pre-sort
2024-02-22T17:49:14Z
0xRobocop marked the issue as sufficient quality report
#1 - c4-judge
2024-03-08T14:34:36Z
0xA5DF marked the issue as grade-b
#2 - c4-judge
2024-03-08T16:08:59Z
0xA5DF marked the issue as selected for report
#3 - c4-judge
2024-03-08T16:09:20Z
0xA5DF marked the issue as grade-a
#4 - thenua3bhai
2024-03-10T07:13:37Z
Hi @0xA5DF Thanks for Judging.
I request you to please re-evaluate gas saved by G-03 in this report. Since it saves significant amount of gas(avg. total ~2596 GAS) instead of currently stated (~100 GAS) in judging sheet. Majorly gas(~2396) will be saved every time when LiquidInfrastructureERC20::mint
function will be called after removing unnecessary nonReentrant
modifier from it.
Since removing nonReentrant
modifier from LiquidInfrastructureERC20::mint
function will save both deployment gas and execution gas every time mint
function is called not only on deployment.
But in judging sheet gas saved by G-03 is mentioned only ~100 GAS.
But removing nonReentrant
modifier from mint
function will save Total avg. of ~2596 GAS considering your method including 1% of deployment gas(1% of ~20000 deployment gas saved -> ~200 GAS) and calling mint
function will also save ~2396 GAS every time this function is called. I have stated gas in report by adding both deployment gas + execution gas, but you are considering only 1% of deployment gas. So even if taking 1% of deployment gas and total execution gas(~2396 GAS) every time mint
function is called. Total avg. gas saved will be ~2596 by G-03.
I have calculated this on sepolia network.
I have explained this in report that reentrancy is not possible in mint
function so we can safely remove nonReentrant
modifier from mint
function. Since in complete logic flow of this mint
function no external call is present not even cross-function. It is not possible to re-enter in mint function.
I am flagging this here even though being selected for report. Since it will make total avg. gas saved by my report from ~7.5k to ~10.1k. Which can change the grade of other grade-a reports to b or c. If same formula you have used in QA to decide other QA reports grades The threshold are 80% of best for grade A, and 60% of best for grade B. will be applied here 2 of the currently grade-a Gas reports can be grade-b and one can be grade-c. Or any other curve you have used to decide other gas reports grade can be used to re-calculate the other 3 reports grade.
Thanks
#5 - 0xA5DF
2024-03-10T09:06:57Z
As you can see in the note in the jduging sheet - this is because this comes at the cost of a security feature. Normally gas optimizations don't affect the functionality of the code. In this case it does, it removes the reentrancy protection, so I don't think it can stand in the same line with normal gas optimizations. I still gave it 100 points because the devs might consider that.
#6 - thenua3bhai
2024-03-10T10:43:36Z
Thanks for your reply. I think it will be completely safe to remove it at no security risk since there is no chance of re-entrancy in that function(reason explained in report). I have checked the logic flow in that function. It is purely removed for significant gas saving purpose at no security/logic risk. I think it can be considered as gas optimization. Thanks
#7 - 0xA5DF
2024-03-10T14:30:49Z
I'm aware of that (otherwise I wouldn't credit the 100 either), but you can never be 100% sure that you don't need that. The reentrancy protection is there to add some additional security, and removing that can't be considered as a normal gas optimization.