[G-02] Using storage instead of memory for struct saves gas
Context:
contracts\JB721TieredGovernance.sol:
156: JBTiered721SetTierDelegatesData memory _data;
contracts\JBTiered721Delegate.sol:
272 // Get a reference to the data being iterated on.
273: JBTiered721MintReservesForTiersData memory _data = _mintReservesForTiersData[_i];
299 // Get a reference to the data being iterated on.
300: JBTiered721MintForTiersData memory _data = _mintForTiersData[_i];
348 // Record the added tiers in the store.
349: uint256[] memory _tierIdsAdded = store.recordAddTiers(_tiersToAdd);
437 // Get a reference to the project's current funding cycle.
438: JBFundingCycle memory _fundingCycle = fundingCycleStore.currentOf(projectId);
439
447 // Record the minted reserves for the tier.
448: uint256[] memory _tokenIds = store.recordMintReservesFor(_tierId, _count);
557 // Keep a reference to the the specific tier IDs to mint.
558: uint16[] memory _tierIdsToMint;
Description:
When fetching data from a storage location, assigning the data to a memory variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read.
[G-03] Functions guaranteed to revert when callled by normal users can be marked payable [24 gas per instance]
Context:
contracts\JBTiered721Delegate.sol:
320 */
321: function adjustTiers(JB721TierParams[] calldata _tiersToAdd, uint256[] calldata _tierIdsToRemove) external override onlyOwner
322 {
366 */
367: function setDefaultReservedTokenBeneficiary(address _beneficiary) external override onlyOwner {
368 // Set the beneficiary.
382 */
383: function setBaseUri(string memory _baseUri) external override onlyOwner {
384 // Store the new value.
398 */
399: function setContractUri(string calldata _contractUri) external override onlyOwner {
400 // Store the new value.
414 */
415: function setTokenUriResolver(IJBTokenUriResolver _tokenUriResolver) external override onlyOwner {
416 // Store the new value.
476 */
477: function mintFor(uint16[] memory _tierIds, address _beneficiary) public override onlyOwner
478 returns (uint256[] memory tokenIds)
Description:
If a function modifier or require such as onlyOwner-admin is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2), DUP1(3), ISZERO(3), PUSH2(3), JUMPI(10), PUSH1(3), DUP1(3), REVERT(0), JUMPDEST(1), POP(2) which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
Recommendation:
Functions guaranteed to revert when called by normal users can be marked payable (for only onlyowner or admin functions)
Proof Of Concept:
The optimizer was turned on and set to 10000 runs.
contract GasTest is DSTest {
Contract0 c0;
Contract1 c1;
function setUp() public {
c0 = new Contract0();
c1 = new Contract1();
}
function testGas() public {
c0.foo();
c1.foo();
}
}
contract Contract0 {
uint256 versionNFTDropCollection;
function foo() external {
versionNFTDropCollection++;
}
}
contract Contract1 {
uint256 versionNFTDropCollection;
function foo() external payable {
versionNFTDropCollection++;
}
}
[G-05] Optimize names to save gas [22 gas per instance]
Context:
All Contracts
Description:
Contracts most called functions could simply save gas by function ordering via Method ID. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions.
Recommendation:
Find a lower method ID name for the most called functions for example Call() vs. Call1() is cheaper by 22 gas
For example, the function IDs in the JBTiered721Delegate.sol contract will be the most used; A lower method ID may be given.
[G-06] Use assembly to write address storage values [33 gas per instance]
Context:
contracts\JBTiered721Delegate.sol:
369 */
370: function setDefaultReservedTokenBeneficiary(address _beneficiary) external override onlyOwner {
371 // Set the beneficiary.
385 */
386: function setBaseUri(string memory _baseUri) external override onlyOwner {
387 // Store the new value.
401 */
402: function setContractUri(string calldata _contractUri) external override onlyOwner {
403 // Store the new value.
417 */
418: function setTokenUriResolver(IJBTokenUriResolver _tokenUriResolver) external override onlyOwner {
419 // Store the new value.
Proof Of Concept:
The optimizer was turned on and set to 10000 runs.
contract GasTestFoundry is DSTest {
Contract1 c1;
Contract2 c2;
function setUp() public {
c1 = new Contract1();
c2 = new Contract2();
}
function testGas() public {
c1.setRewardTokenAndAmount(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,356);
c2.setRewardTokenAndAmount(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,356);
}
}
contract Contract1 {
address rewardToken ;
uint256 reward;
function setRewardTokenAndAmount(address token_, uint256 reward_) external {
rewardToken = token_;
reward = reward_;
}
}
contract Contract2 {
address rewardToken ;
uint256 reward;
function setRewardTokenAndAmount(address token_, uint256 reward_) external {
assembly {
sstore(rewardToken.slot, token_)
sstore(reward.slot, reward_)
}
}
}
[G-07] The solady Library's Ownable contract is significantly gas-optimized, which can be used
Description:
The project uses the onlyOwner authorization model with the PendingOwnable.sol contract. I recommend using Solady's highly gas optimized contract.
Description:
You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. Making the constructor payable eliminates the need for an initial check of msg.value == 0 and saves 13 gas on deployment with no security risks.
Description:
Missing checks for zero-addresses may lead to infunctional protocol, if the variable addresses are updated incorrectly. It also wast gas as it requires the redeployment of the contract.
Gas Optimizations List
payable
x -= y (x += y)
costs more gas thanx = x – y (x = x + y)
for state variablesaddress(0)
payable
Total 14 issues
Suggestions
v4.8.0 OpenZeppelin
contractszero-address
check inconstructor
Total 2 suggestions
[G-01] Changing state variables to Immutable is gas-optimized
Context:
Description: Changing state variables to immutable is ~16k gas-optimized in terms of deployment cost.
Gas Report: Deployment
[G-02] Using storage instead of memory for struct saves gas
Context:
Description: When fetching data from a storage location, assigning the data to a memory variable causes all fields of the struct/array to be read from storage, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory variable, they incur an additional MLOAD rather than a cheap stack read.
[G-03] Functions guaranteed to revert when callled by normal users can be marked
payable
[24 gas per instance]Context:
Description: If a function modifier or require such as onlyOwner-admin is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2), DUP1(3), ISZERO(3), PUSH2(3), JUMPI(10), PUSH1(3), DUP1(3), REVERT(0), JUMPDEST(1), POP(2) which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
Recommendation: Functions guaranteed to revert when called by normal users can be marked payable (for only
onlyowner or admin
functions)Proof Of Concept: The optimizer was turned on and set to 10000 runs.
Gas Report:
[G-04]
x -= y (x += y)
costs more gas thanx = x – y (x = x + y)
for state variables [32 gas per instance]Context:
Description:
x -= y
costs more gas thanx = x – y
for state variables.Proof Of Concept: The optimizer was turned on and set to 10000 runs.
Gas Report:
[G-05] Optimize names to save gas [22 gas per instance]
Context: All Contracts
Description: Contracts most called functions could simply save gas by function ordering via
Method ID
. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because22 gas
are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions.Recommendation: Find a lower
method ID
name for the most called functions for example Call() vs. Call1() is cheaper by22 gas
For example, the function IDs in theJBTiered721Delegate.sol
contract will be the most used; A lower method ID may be given.Proof of Consept: https://medium.com/joyso/solidity-how-does-function-name-affect-gas-consumption-in-smart-contract-47d270d8ac92
JBTiered721Delegate.sol function names can be named and sorted according to METHOD ID
[G-06] Use
assembly
to write address storage values [33 gas per instance]Context:
Proof Of Concept: The optimizer was turned on and set to 10000 runs.
Gas Report:
[G-07] The solady Library's Ownable contract is significantly gas-optimized, which can be used
Description: The project uses the
onlyOwner
authorization model with thePendingOwnable.sol
contract. I recommend using Solady's highly gas optimized contract.https://github.com/Vectorized/solady/blob/main/src/auth/OwnableRoles.sol
[G-08] Setting the constructor to
payable
[13 gas per instance]Context:
Description: You can cut out 10 opcodes in the creation-time EVM bytecode if you declare a constructor payable. Making the constructor payable eliminates the need for an initial check of
msg.value == 0
and saves13 gas
on deployment with no security risks.Recommendation: Set the constructor to
payable
Proof Of Concept: https://forum.openzeppelin.com/t/a-collection-of-gas-optimisation-tricks/19966/5?u=pcaversaccio
The optimizer was turned on and set to 10000 runs
Gas Report
[S-01] Use
v4.8.0 OpenZeppelin
contractsDescription: The upcoming v4.8.0 version of OpenZeppelin provides many small gas optimizations.
https://github.com/OpenZeppelin/openzeppelin-contracts/releases/tag/v4.8.0-rc.0
[S-02] Missing
zero-address
check inconstructor
Description: Missing checks for zero-addresses may lead to infunctional protocol, if the variable addresses are updated incorrectly. It also wast gas as it requires the redeployment of the contract.