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: 29/84
Findings: 1
Award: $116.28
π Selected for report: 0
π Solo Findings: 0
π Selected for report: 0x11singh99
Also found by: 0xSmartContractSamurai, c3phas, dharma09
116.2791 USDC - $116.28
Highlighted below are optimizations exclusively targeting state-mutating functions and view/pure functions invoked by state-mutating functions. In the discussion that follows, only runtime gas is emphasized, given its inevitable dominance over deployment gas costs throughout the protocol's lifetime.
Please be aware that some code snippets may be shortened to conserve space, and certain code snippets may include @audit tags in comments to facilitate issue explanations.
Details: Instead of repeatedly calling this.balanceOf(recipient) in the inner loop, you can calculate it once and use the result.
Proof of Code
File: liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol 214: 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); //@audit cache outside loop if (toDistribute.transfer(recipient, entitlement)) { receipts[j] = entitlement; } } emit Distribution(recipient, distributableERC20s, receipts); } } nextDistributionRecipient = i; if (nextDistributionRecipient == holders.length) { _endDistribution(); }
Optimized code:
diff --git a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol index 4722279..c519f12 100644 --- a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol +++ b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol @@ -212,15 +212,16 @@ contract LiquidInfrastructureERC20 is uint i; for (i = nextDistributionRecipient; i < limit; i++) { address recipient = holders[i]; if (isApprovedHolder(recipient)) { uint256[] memory receipts = new uint256[]( distributableERC20s.length ); + recipientBalance = this.balanceOf(recipient); for (uint j = 0; j < distributableERC20s.length; j++) { IERC20 toDistribute = IERC20(distributableERC20s[j]); uint256 entitlement = erc20EntitlementPerUnit[j] * - this.balanceOf(recipient); + recipientBalance ; if (toDistribute.transfer(recipient, entitlement)) { receipts[j] = entitlement; }
string.concat
if there is only one string use abi.encodePacked
Details If there is only one string being concatenated, you can avoid using string.concat and simply concatenate the strings directly. This may slightly reduce gas consumption. Proof of Code
File: liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol 62: constructor( string memory accountName ) ERC721( string.concat( "althea://liquid-infrastructure-account/", accountName ), string.concat("LIA:", accountName) //@audit do not use string.concat if there is only one string ) { _mint(msg.sender, AccountId); }
Optimized code:
constructor(string memory accountName) ERC721( - string.concat( - "althea://liquid-infrastructure-account/", - accountName - ), - string.concat("LIA:", accountName) + string(abi.encodePacked("althea://liquid-infrastructure-account/", accountName)), + string(abi.encodePacked("LIA:", accountName)) ) { _mint(msg.sender, AccountId); }
Details Instead of calling holders.length twice, calculate it once and use the result.
Optimized code:
File: liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol 184: function distributeToAllHolders() public { uint256 num = holders.length; if (num > 0) { - distribute(holders.length); //@audit + distribute(num); } }
.length
outside the loop to avoid multiple recalculations during each iteration.Details Cache the ManagedNFTs.length outside the loop to avoid multiple recalculations during each iteration. Proof of Code
File: liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol 360: function withdrawFromManagedNFTs(uint256 numWithdrawals) public { require(!LockedForDistribution, "cannot withdraw during distribution"); if (nextWithdrawal == 0) { emit WithdrawalStarted(); } uint256 limit = Math.min( numWithdrawals + nextWithdrawal, ManagedNFTs.length //@audit cache ManagedNFTs.length ); .... nextWithdrawal = i; if (nextWithdrawal == ManagedNFTs.length) { nextWithdrawal = 0; emit WithdrawalFinished(); } }
Optimized code:
diff --git a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol index 4722279..ce52c5d 100644 --- a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol +++ b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol @@ -362,10 +363,10 @@ contract LiquidInfrastructureERC20 is if (nextWithdrawal == 0) { emit WithdrawalStarted(); } - + uint256 managedNFTsLength = ManagedNFTs.length; uint256 limit = Math.min( numWithdrawals + nextWithdrawal, - ManagedNFTs.length + managedNFTsLength ); uint256 i; for (i = nextWithdrawal; i < limit; i++) { @@ -379,7 +380,7 @@ contract LiquidInfrastructureERC20 is } nextWithdrawal = i; - if (nextWithdrawal == ManagedNFTs.length) { + if (nextWithdrawal == managedNFTsLength) { nextWithdrawal = 0; emit WithdrawalFinished(); }
Details Cache the ManagedNFTs.length outside the loop to avoid multiple recalculations during each iteration. Proof of Code
File: liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol 414: 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]; //@audit no need to cache make variable if (managed == nftContract) { // Delete by copying in the last element and then pop the end ManagedNFTs[i] = ManagedNFTs[ManagedNFTs.length - 1]; //@audit cache ManagedNFTs.length 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); }
Optimized code:
diff --git a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol index 4722279..02c44b9 100644 --- a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol +++ b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol @@ -416,13 +417,13 @@ contract LiquidInfrastructureERC20 is ) 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 (uint i = 0; i < ManagedNFTs.length; i++) { + for (uint i = 0; i < managedNFTsLength; 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; }
Details Cache the newErc20s.length outside the loop to avoid multiple recalculations during each iteration. Proof of Code
File: liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol 108: function setThresholds( address[] calldata newErc20s, uint256[] calldata newAmounts ) public virtual onlyOwnerOrApproved(AccountId) { require( newErc20s.length == newAmounts.length, "threshold values must have the same length" ); // Clear the thresholds before overwriting delete thresholdErc20s; delete thresholdAmounts; for (uint i = 0; i < newErc20s.length; i++) { //@audit cache length thresholdErc20s.push(newErc20s[i]); thresholdAmounts.push(newAmounts[i]); } emit ThresholdsChanged(newErc20s, newAmounts); }
Optimized code:
diff --git a/liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol b/liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol index 0573e34..f3d7a20 100644 --- a/liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol +++ b/liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol @@ -109,15 +109,16 @@ contract LiquidInfrastructureNFT is ERC721, OwnableApprovableERC721 { address[] calldata newErc20s, uint256[] calldata newAmounts ) public virtual onlyOwnerOrApproved(AccountId) { + uint256 newErc20s = newErc20s.length; require( - newErc20s.length == newAmounts.length, + newErc20s == newAmounts.length, "threshold values must have the same length" ); // Clear the thresholds before overwriting delete thresholdErc20s; delete thresholdAmounts; - for (uint i = 0; i < newErc20s.length; i++) { + for (uint i = 0; i < newErc20s; i++) { //@audit cache length thresholdErc20s.push(newErc20s[i]); thresholdAmounts.push(newAmounts[i]); }
Details Cache the erc20s.length outside the loop to avoid multiple recalculations during each iteration. Proof of Code
File: liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol 170: function _withdrawBalancesTo( address[] calldata erc20s, address destination ) internal { uint256[] memory amounts = new uint256[](erc20s.length); for (uint i = 0; i < erc20s.length; i++) { //@audit address erc20 = erc20s[i]; uint256 balance = IERC20(erc20).balanceOf(address(this)); if (balance > 0) { bool result = IERC20(erc20).transfer(destination, balance); require(result, "unsuccessful withdrawal"); amounts[i] = balance; } } emit SuccessfulWithdrawal(destination, erc20s, amounts); }
Optimized code:
diff --git a/liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol b/liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol index 0573e34..7a21269 100644 --- a/liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol +++ b/liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol @@ -171,9 +171,10 @@ contract LiquidInfrastructureNFT is ERC721, OwnableApprovableERC721 { address[] calldata erc20s, address destination ) internal { + uint256 erc20s = erc20s.length; uint256[] memory amounts = new uint256[](erc20s.length); - for (uint i = 0; i < erc20s.length; i++) { - address erc20 = erc20s[i]; + for (uint i = 0; i < erc20s; i++) { //@audit + address erc20 = erc20s[i]; uint256 balance = IERC20(erc20).balanceOf(address(this)); if (balance > 0) { bool result = IERC20(erc20).transfer(destination, balance);
Details Cache the holders.length outside the loop to avoid multiple recalculations during each iteration. Proof of Code
File: liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol 164: 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++) { //@audit 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(); } } } }
Optimized code:
diff --git a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol index 4722279..bc3bbf4 100644 --- a/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol +++ b/liquid-infrastructure/contracts/LiquidInfrastructureERC20.sol @@ -168,10 +168,11 @@ contract LiquidInfrastructureERC20 is ) internal virtual override { bool stillHolding = (this.balanceOf(from) != 0); if (!stillHolding) { - for (uint i = 0; i < holders.length; i++) { + uint256 holder= holders.length; + for (uint i = 0; i < holder; 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[i] = holders[holder - 1]; holders.pop();
Details creating variables outside the loop can potentially save gas. Proof of Code
File: liquid-infrastructure/contracts/LiquidInfrastructureNFT.sol 176: address erc20 = erc20s[i]; //@audit make variable outside loop
File: 215: address recipient = holders[i]; //@audit make variable outside loop
#0 - c4-pre-sort
2024-02-22T17:46:12Z
0xRobocop marked the issue as sufficient quality report
#1 - c4-judge
2024-03-08T14:36:04Z
0xA5DF marked the issue as grade-c
#2 - Dharma-09
2024-03-08T22:34:26Z
Hello @0xA5DF , thank you for your time and effort in reviewing my report. I believe #723 qualifies for a grade-a because the most of the instances save large amounts of gas, particularly [G-1], which you also note in #722, and [G-4], which finds cache length in a local variable, will save a decent amount of gas. My request is to recheckΒ my submission.Thank you for reviewing it.
#3 - c4-judge
2024-03-09T08:26:23Z
0xA5DF marked the issue as grade-a
#4 - 0xA5DF
2024-03-09T08:27:32Z
You're right about G1, I've upgraded this to grade A About G4 - this saves only one call (100 gas units), the lines you're optimizing aren't inside a loop