Llama - JCN's results

A governance system for onchain organizations.

General Information

Platform: Code4rena

Start Date: 06/06/2023

Pot Size: $60,500 USDC

Total HM: 5

Participants: 50

Period: 8 days

Judge: gzeon

Id: 246

League: ETH

Llama

Findings Distribution

Researcher Performance

Rank: 15/50

Findings: 1

Award: $361.13

Gas:
grade-a

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

Awards

361.1341 USDC - $361.13

Labels

bug
G (Gas Optimization)
grade-a
selected for report
G-11

External Links

Summary

A majority of the optimizations were benchmarked via the protocol's tests, i.e. using the following config: solc version 0.8.17, optimizer on, and 1300 runs. Optimizations that were not benchmarked are explained via EVM gas costs and opcodes.

Note

  • Only optimizations for state-mutating functions (i.e. non view/pure) and view/pure functions called within state-mutating functions have been highlighted below.
  • Some code snippets may be truncated to save space. Code snippets may also be accompanied by @audit tags in comments to aid in explaining the issue.

Gas Optimizations

NumberIssueInstancesGas Saved
G-01State variables can be cached instead of re-reading them from storage5500
G-02Cache state variables outside of loop to avoid reading/writing storage on every iteration35869
G-03Multiple address mappings can be combined into a single mapping of an address to a struct, where appropriate121838
G-04Cache calldata/memory pointers for complex types to avoid offset calculations21192
G-05Forgo internal function to save 1 STATICCALL4400
G-06Multiple accesses of a mapping/array should use a local variable cache1116
G-07Refactor If/require statements to save SLOADs in case of early revert4-

Total Estimated Gas Saved: 29915

State variables can be cached instead of re-reading them from storage

Caching of a state variable replaces each Gwarmaccess (100 gas) with a much cheaper stack read.

Total Instances: 5

Estimated Gas Saved: 5 * 100 = 500

Note: These are instances missed by the Automated Report.

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaCore.sol#L542-L559

Use already cached actionId to save 1 SLOAD

File: src/LlamaCore.sol
542:    actionId = actionsCount; // @audit: 1st sload
...
559:    actionsCount = LlamaUtils.uncheckedIncrement(actionsCount); // Safety: Can never overflow a uint256 by incrementing. // @audit: 2nd sload
diff --git a/src/LlamaCore.sol b/src/LlamaCore.sol
index 89d60de..049f66d 100644
--- a/src/LlamaCore.sol
+++ b/src/LlamaCore.sol
@@ -556,7 +556,7 @@ contract LlamaCore is Initializable {
       newAction.isScript = authorizedScripts[target];
     }

-    actionsCount = LlamaUtils.uncheckedIncrement(actionsCount); // Safety: Can never overflow a uint256 by incrementing.
+    actionsCount = LlamaUtils.uncheckedIncrement(actionId); // Safety: Can never overflow a uint256 by incrementing.

     emit ActionCreated(actionId, policyholder, role, strategy, target, value, data, description);
   }

https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaAbsolutePeerReview.sol#L56-L58

Cache minDisapprovals to save 1 SLOAD

File: src/strategies/LlamaAbsolutePeerReview.sol
56:      if (
57:        minDisapprovals != type(uint128).max // @audit: 1st sload
58:          && minDisapprovals > disapprovalPolicySupply - actionCreatorDisapprovalRoleQty // @audit: 2nd sload
diff --git a/src/strategies/LlamaAbsolutePeerReview.sol b/src/strategies/LlamaAbsolutePeerReview.sol
index 85feb92..c8426aa 100644
--- a/src/strategies/LlamaAbsolutePeerReview.sol
+++ b/src/strategies/LlamaAbsolutePeerReview.sol
@@ -53,9 +53,10 @@ contract LlamaAbsolutePeerReview is LlamaAbsoluteStrategyBase {
       if (minApprovals > approvalPolicySupply - actionCreatorApprovalRoleQty) revert InsufficientApprovalQuantity();

       uint256 actionCreatorDisapprovalRoleQty = llamaPolicy.getQuantity(actionInfo.creator, disapprovalRole);
+      uint128 _minDisapprovals = minDisapprovals;
       if (
-        minDisapprovals != type(uint128).max
-          && minDisapprovals > disapprovalPolicySupply - actionCreatorDisapprovalRoleQty
+        _minDisapprovals != type(uint128).max
+          && _minDisapprovals > disapprovalPolicySupply - actionCreatorDisapprovalRoleQty
       ) revert InsufficientDisapprovalQuantity();
     }
   }

https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaRelativeQuorum.sol#L220-L223

Cache forceApprovalRole[role] to save 1 SLOAD

File: src/strategies/LlamaRelativeQuorum.sol
220:  function getApprovalQuantityAt(address policyholder, uint8 role, uint256 timestamp) external view returns (uint128) {
221:    if (role != approvalRole && !forceApprovalRole[role]) return 0; // @audit: 1st sload
222:    uint128 quantity = policy.getPastQuantity(policyholder, role, timestamp);
223:    return quantity > 0 && forceApprovalRole[role] ? type(uint128).max : quantity; // @audit: 2nd sload
diff --git a/src/strategies/LlamaRelativeQuorum.sol b/src/strategies/LlamaRelativeQuorum.sol
index d796ae9..8d74c92 100644
--- a/src/strategies/LlamaRelativeQuorum.sol
+++ b/src/strategies/LlamaRelativeQuorum.sol
@@ -218,9 +218,10 @@ contract LlamaRelativeQuorum is ILlamaStrategy, Initializable {

   /// @inheritdoc ILlamaStrategy
   function getApprovalQuantityAt(address policyholder, uint8 role, uint256 timestamp) external view returns (uint128) {
-    if (role != approvalRole && !forceApprovalRole[role]) return 0;
+    bool _forceApprovalRole = forceApprovalRole[role];
+    if (role != approvalRole && !_forceApprovalRole) return 0;
     uint128 quantity = policy.getPastQuantity(policyholder, role, timestamp);
-    return quantity > 0 && forceApprovalRole[role] ? type(uint128).max : quantity;
+    return quantity > 0 && _forceApprovalRole ? type(uint128).max : quantity;
   }

https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaRelativeQuorum.sol#L240-L242

Cache forceDisapprovalRole[role] to save 1 SLOAD

File: src/strategies/LlamaRelativeQuorum.sol
240:    if (role != disapprovalRole && !forceDisapprovalRole[role]) return 0; // @audit: 1st sload
241:    uint128 quantity = policy.getPastQuantity(policyholder, role, timestamp);
242:    return quantity > 0 && forceDisapprovalRole[role] ? type(uint128).max : quantity; // @audit: 2nd sload
diff --git a/src/strategies/LlamaRelativeQuorum.sol b/src/strategies/LlamaRelativeQuorum.sol
index d796ae9..e1c3927 100644
--- a/src/strategies/LlamaRelativeQuorum.sol
+++ b/src/strategies/LlamaRelativeQuorum.sol
@@ -237,9 +237,10 @@ contract LlamaRelativeQuorum is ILlamaStrategy, Initializable {
     view
     returns (uint128)
   {
-    if (role != disapprovalRole && !forceDisapprovalRole[role]) return 0;
+    bool _forceDisapprovalRole = forceDisapprovalRole[role];
+    if (role != disapprovalRole && !_forceDisapprovalRole) return 0;
     uint128 quantity = policy.getPastQuantity(policyholder, role, timestamp);
-    return quantity > 0 && forceDisapprovalRole[role] ? type(uint128).max : quantity;
+    return quantity > 0 && _forceDisapprovalRole ? type(uint128).max : quantity;
   }

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaFactory.sol#L260-L263

Cache llamaCount to save 1 SLOAD

File: src/LlamaFactory.sol
260:    emit LlamaInstanceCreated(
261:      llamaCount, name, address(llamaCore), address(llamaExecutor), address(policy), block.chainid // @audit: 1st sload
262:    );
263:    llamaCount = LlamaUtils.uncheckedIncrement(llamaCount); // @audit: 2nd sload
diff --git a/src/LlamaFactory.sol b/src/LlamaFactory.sol
index 0cc4cfd..269e4cb 100644
--- a/src/LlamaFactory.sol
+++ b/src/LlamaFactory.sol
@@ -256,11 +256,12 @@ contract LlamaFactory {
     llamaExecutor = llamaCore.executor();

     policy.finalizeInitialization(address(llamaExecutor), bootstrapPermissionId);
-
+
+    uint256 _llamaCount = llamaCount;
     emit LlamaInstanceCreated(
-      llamaCount, name, address(llamaCore), address(llamaExecutor), address(policy), block.chainid
+      _llamaCount, name, address(llamaCore), address(llamaExecutor), address(policy), block.chainid
     );
-    llamaCount = LlamaUtils.uncheckedIncrement(llamaCount);
+    llamaCount = LlamaUtils.uncheckedIncrement(_llamaCount);
   }

Cache state variables outside of loop to avoid reading/writing storage on every iteration

Reading from storage should always try to be avoided within loops. In the following instances, we are able to cache state variables outside of the loop to save a Gwarmaccess (100 gas) per loop iteration. In addition, for some instances we are also able to increment the cached variable in the loop and update the storage variable outside the loop to save 1 SSTORE per loop iteration.

Total Instances: 3

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaPolicy.sol#L227

Gas Savings for LlamaPolicy.revokePolicy, obtained via protocol's tests: Avg 724 gas

MedMaxAvg# calls
Before690401100675983711
Before681801089925911311

Cache numRoles outside of loop to save 1 SLOAD per iteration

File: src/LlamaPolicy.sol
227    for (uint256 i = 1; i <= numRoles; i = LlamaUtils.uncheckedIncrement(i)) { // @audit: numRoles read on every iteration
diff --git a/src/LlamaPolicy.sol b/src/LlamaPolicy.sol
index 3fca63e..7e47189 100644
--- a/src/LlamaPolicy.sol
+++ b/src/LlamaPolicy.sol
@@ -224,7 +224,8 @@ contract LlamaPolicy is ERC721NonTransferableMinimalProxy {
     // We start from i = 1 here because a value of zero is reserved for the "all holders" role, and
     // that will get removed automatically when the token is burned. Similarly, use we `<=` to make sure
     // the last role is also revoked.
-    for (uint256 i = 1; i <= numRoles; i = LlamaUtils.uncheckedIncrement(i)) {
+    uint8 _numRoles = numRoles;
+    for (uint256 i = 1; i <= _numRoles; i = LlamaUtils.uncheckedIncrement(i)) {
       if (hasRole(policyholder, uint8(i))) _setRoleHolder(uint8(i), policyholder, 0, 0);
     }
     _burn(_tokenId(policyholder));

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaPolicy.sol#L151-L168

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaPolicy.sol#L393-L396

Gas Savings for LlamaFactory.deploy, obtained via protocol's tests: Avg 4468 gas

MedMaxAvg# calls
Before510115754064255015882412
After509689352818115011414412

To benchmark this instance we will bring the logic from _initializeRole into the construtor in order to refactor the logic. Note that another way of achieving this is by refactoring the logic of the _initializeRole directly and every other function that calls _initializeRole.

Cache numRoles outside loop, increment cached variable in loop, and update storage outside loop to save 2 SLOADs + 1 SSTORE per iteration

File: src/LlamaPolicy.sol
151:    for (uint256 i = 0; i < roleDescriptions.length; i = LlamaUtils.uncheckedIncrement(i)) {
152:      _initializeRole(roleDescriptions[i]); // @audit: sload & sstore for `numRoles` on every iteration
153:    }
...
168:    if (numRoles == 0 || getRoleSupplyAsNumberOfHolders(ALL_HOLDERS_ROLE) == 0) revert InvalidRoleHolderInput(); // @audit: sload for `numRoles`

393:  function _initializeRole(RoleDescription description) internal {
394:    numRoles += 1; // @audit: sload + sstore for `numRoles`
395:    emit RoleInitialized(numRoles, description); // @audit: 2nd sload 
  }
diff --git a/src/LlamaPolicy.sol b/src/LlamaPolicy.sol
index 3fca63e..af2129b 100644
--- a/src/LlamaPolicy.sol
+++ b/src/LlamaPolicy.sol
@@ -148,9 +148,12 @@ contract LlamaPolicy is ERC721NonTransferableMinimalProxy {
   ) external initializer {
     __initializeERC721MinimalProxy(_name, string.concat("LL-", LibString.replace(LibString.upper(_name), " ", "-")));
     factory = LlamaFactory(msg.sender);
+    uint8 _numRoles = numRoles;
     for (uint256 i = 0; i < roleDescriptions.length; i = LlamaUtils.uncheckedIncrement(i)) {
-      _initializeRole(roleDescriptions[i]);
+        _numRoles += 1;
+        emit RoleInitialized(_numRoles, roleDescriptions[i]);
     }
+    numRoles = _numRoles;

     for (uint256 i = 0; i < roleHolders.length; i = LlamaUtils.uncheckedIncrement(i)) {
       _setRoleHolder(
@@ -165,7 +168,7 @@ contract LlamaPolicy is ERC721NonTransferableMinimalProxy {
     // Must have assigned roles during initialization, otherwise the system cannot be used. However,
     // we do not check that roles were assigned "properly" as there is no single correct way, so
     // this is more of a sanity check, not a guarantee that the system will work after initialization.
-    if (numRoles == 0 || getRoleSupplyAsNumberOfHolders(ALL_HOLDERS_ROLE) == 0) revert InvalidRoleHolderInput();
+    if (_numRoles == 0 || getRoleSupplyAsNumberOfHolders(ALL_HOLDERS_ROLE) == 0) revert InvalidRoleHolderInput();
   }

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaPolicy.sol#L161-L163

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaPolicy.sol#L490-L491

Gas Savings for LlamaFactory.deploy, obtained via protocol's tests: Avg 677 gas

MedMaxAvg# calls
Before510115754064255015882412
After510117551201195015205412

To benchmark this instance we will refactor the logic of the _setRolePermission internal function directly and also refactor every other function that calls _setRolePermission. Another way of achieving this would be to move the logic of _setRolePermission into the construtor and refactoring it there.

Cache numRoles outside loop to save 1 SLOAD per iteration

File: src/LlamaPolicy.sol
161:    for (uint256 i = 0; i < rolePermissions.length; i = LlamaUtils.uncheckedIncrement(i)) {
162:      _setRolePermission(rolePermissions[i].role, rolePermissions[i].permissionId, rolePermissions[i].hasPermission); // @audit: sload for `numRoles` on every iteration
163:    }

490:  function _setRolePermission(uint8 role, bytes32 permissionId, bool hasPermission) internal {
491:    if (role > numRoles) revert RoleNotInitialized(role); // @audit: sload for `numRoles`
diff --git a/src/LlamaPolicy.sol b/src/LlamaPolicy.sol
index 3fca63e..8a3273a 100644
--- a/src/LlamaPolicy.sol
+++ b/src/LlamaPolicy.sol
@@ -157,15 +157,16 @@ contract LlamaPolicy is ERC721NonTransferableMinimalProxy {
         roleHolders[i].role, roleHolders[i].policyholder, roleHolders[i].quantity, roleHolders[i].expiration
       );
     }
-
+
+    uint8 _numRoles = numRoles;
     for (uint256 i = 0; i < rolePermissions.length; i = LlamaUtils.uncheckedIncrement(i)) {
-      _setRolePermission(rolePermissions[i].role, rolePermissions[i].permissionId, rolePermissions[i].hasPermission);
+      _setRolePermission(_numRoles, rolePermissions[i].role, rolePermissions[i].permissionId, rolePermissions[i].hasPermission);
     }

     // Must have assigned roles during initialization, otherwise the system cannot be used. However,
     // we do not check that roles were assigned "properly" as there is no single correct way, so
     // this is more of a sanity check, not a guarantee that the system will work after initialization.
-    if (numRoles == 0 || getRoleSupplyAsNumberOfHolders(ALL_HOLDERS_ROLE) == 0) revert InvalidRoleHolderInput();
+    if (_numRoles == 0 || getRoleSupplyAsNumberOfHolders(ALL_HOLDERS_ROLE) == 0) revert InvalidRoleHolderInput();
   }

   // ===========================================
@@ -181,7 +182,7 @@ contract LlamaPolicy is ERC721NonTransferableMinimalProxy {
     if (llamaExecutor != address(0)) revert AlreadyInitialized();

     llamaExecutor = _llamaExecutor;
-    _setRolePermission(BOOTSTRAP_ROLE, bootstrapPermissionId, true);
+    _setRolePermission(numRoles, BOOTSTRAP_ROLE, bootstrapPermissionId, true);
   }

   // -------- Role and Permission Management --------
@@ -205,7 +206,7 @@ contract LlamaPolicy is ERC721NonTransferableMinimalProxy {
   /// @param permissionId Permission ID to assign to the role.
   /// @param hasPermission Whether to assign the permission or remove the permission.
   function setRolePermission(uint8 role, bytes32 permissionId, bool hasPermission) external onlyLlama {
-    _setRolePermission(role, permissionId, hasPermission);
+    _setRolePermission(numRoles, role, permissionId, hasPermission);
   }

   /// @notice Revokes a policyholder's expired role.
@@ -487,8 +488,8 @@ contract LlamaPolicy is ERC721NonTransferableMinimalProxy {
   }

   /// @dev Sets a role's permission along with whether that permission is valid or not.
-  function _setRolePermission(uint8 role, bytes32 permissionId, bool hasPermission) internal {
-    if (role > numRoles) revert RoleNotInitialized(role);
+  function _setRolePermission(uint8 _numRoles, uint8 role, bytes32 permissionId, bool hasPermission) internal {
+    if (role > _numRoles) revert RoleNotInitialized(role);
     canCreateAction[role][permissionId] = hasPermission;
     emit RolePermissionAssigned(role, permissionId, hasPermission);
   }

Multiple address mappings can be combined into a single mapping of an address to a struct, where appropriate

We can combine multiple mappings below into structs. This will result in cheaper storage reads since multiple mappings are accessed in functions and those values are now occupying the same storage slot, meaning the slot will become warm after the first SLOAD. In addition, when writing to and reading from the struct values we will avoid a Gsset (20000 gas) and Gcoldsload (2100 gas) since multiple struct values are now occupying the same slot.

Note: This instance was missed by the automated report.

https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaRelativeQuorum.sol#L130-L133

Gas Savings for LlamaCore.executeAction, obtained via protocol's tests: Avg 21838 gas

MedMaxAvg# calls
Before5186172238195704807541430
After5164334320815894803180430
File: src/strategies/LlamaRelativeQuorum.sol
130:  mapping(uint8 => bool) public forceApprovalRole;
131:
132:  /// @notice Mapping of roles that can force an action to be disapproved.
133:  mapping(uint8 => bool) public forceDisapprovalRole;
diff --git a/src/strategies/LlamaRelativeQuorum.sol b/src/strategies/LlamaRelativeQuorum.sol
index d796ae9..2cbeb0c 100644
--- a/src/strategies/LlamaRelativeQuorum.sol
+++ b/src/strategies/LlamaRelativeQuorum.sol
@@ -125,12 +125,13 @@ contract LlamaRelativeQuorum is ILlamaStrategy, Initializable {

   /// @notice The role that can disapprove an action.
   uint8 public disapprovalRole;
+
+  struct ForceRoles {
+    bool forceApprovalRole;
+    bool forceDisapprovalRole;
+  }

-  /// @notice Mapping of roles that can force an action to be approved.
-  mapping(uint8 => bool) public forceApprovalRole;
-
-  /// @notice Mapping of roles that can force an action to be disapproved.
-  mapping(uint8 => bool) public forceDisapprovalRole;
+  mapping(uint8 => ForceRoles) forceRoles;

   /// @notice Mapping of action ID to the supply of the approval role at the time the action was created.
   mapping(uint256 => uint256) public actionApprovalSupply;
@@ -146,6 +147,15 @@ contract LlamaRelativeQuorum is ILlamaStrategy, Initializable {
     _disableInitializers();
   }

+  // @audit: Getters used for benchmarking purposes
+  function forceApprovalRole(uint8 role) external view returns (bool) {
+    return forceRoles[role].forceApprovalRole;
+  }
+
+  function forceDisapprovalRole(uint8 role) external view returns (bool) {
+    return forceRoles[role].forceDisapprovalRole;
+  }
+
   // ==========================================
   // ======== Interface Implementation ========
   // ==========================================
@@ -178,7 +188,7 @@ contract LlamaRelativeQuorum is ILlamaStrategy, Initializable {
       uint8 role = strategyConfig.forceApprovalRoles[i];
       if (role == 0) revert InvalidRole(0);
       _assertValidRole(role, numRoles);
-      forceApprovalRole[role] = true;
+      forceRoles[role].forceApprovalRole = true;
       emit ForceApprovalRoleAdded(role);
     }

@@ -186,7 +196,7 @@ contract LlamaRelativeQuorum is ILlamaStrategy, Initializable {
       uint8 role = strategyConfig.forceDisapprovalRoles[i];
       if (role == 0) revert InvalidRole(0);
       _assertValidRole(role, numRoles);
-      forceDisapprovalRole[role] = true;
+      forceRoles[role].forceDisapprovalRole = true;
       emit ForceDisapprovalRoleAdded(role);
     }

@@ -213,14 +223,14 @@ contract LlamaRelativeQuorum is ILlamaStrategy, Initializable {

   /// @inheritdoc ILlamaStrategy
   function isApprovalEnabled(ActionInfo calldata, address, uint8 role) external view {
-    if (role != approvalRole && !forceApprovalRole[role]) revert InvalidRole(approvalRole);
+    if (role != approvalRole && !forceRoles[role].forceApprovalRole) revert InvalidRole(approvalRole);
   }

   /// @inheritdoc ILlamaStrategy
   function getApprovalQuantityAt(address policyholder, uint8 role, uint256 timestamp) external view returns (uint128) {
-    if (role != approvalRole && !forceApprovalRole[role]) return 0;
+    if (role != approvalRole && !forceRoles[role].forceApprovalRole) return 0;
     uint128 quantity = policy.getPastQuantity(policyholder, role, timestamp);
-    return quantity > 0 && forceApprovalRole[role] ? type(uint128).max : quantity;
+    return quantity > 0 && forceRoles[role].forceApprovalRole ? type(uint128).max : quantity;
   }

   // -------- When Casting Disapproval --------
@@ -228,7 +238,7 @@ contract LlamaRelativeQuorum is ILlamaStrategy, Initializable {
   /// @inheritdoc ILlamaStrategy
   function isDisapprovalEnabled(ActionInfo calldata, address, uint8 role) external view {
     if (minDisapprovalPct > ONE_HUNDRED_IN_BPS) revert DisapprovalDisabled();
-    if (role != disapprovalRole && !forceDisapprovalRole[role]) revert InvalidRole(disapprovalRole);
+    if (role != disapprovalRole && !forceRoles[role].forceDisapprovalRole) revert InvalidRole(disapprovalRole);
   }

   /// @inheritdoc ILlamaStrategy
@@ -237,9 +247,9 @@ contract LlamaRelativeQuorum is ILlamaStrategy, Initializable {
     view
     returns (uint128)
   {
-    if (role != disapprovalRole && !forceDisapprovalRole[role]) return 0;
+    if (role != disapprovalRole && !forceRoles[role].forceDisapprovalRole) return 0;
     uint128 quantity = policy.getPastQuantity(policyholder, role, timestamp);
-    return quantity > 0 && forceDisapprovalRole[role] ? type(uint128).max : quantity;
+    return quantity > 0 && forceRoles[role].forceDisapprovalRole ? type(uint128).max : quantity;
   }

   // -------- When Queueing --------

Cache calldata/memory pointers for complex types to avoid offset calculations

The function parameters in the following instances are complex types (i.e. arrays which contain structs) and thus will result in more complex offset calculations to retrieve specific data from calldata/memory. We can avoid peforming some of these offset calculations by instantiating calldata/memory pointers.

Total Instances: 2

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaPolicy.sol#L155-L159

Gas Savings for LlamaPolicy.deploy, obtained via protocol's tests: Avg 484 gas

MedMaxAvg# calls
Before510115754064255015882412
After510103452565895015398412
File: src/LlamaPolicy.sol
155:    for (uint256 i = 0; i < roleHolders.length; i = LlamaUtils.uncheckedIncrement(i)) {
156:      _setRoleHolder(
157:        roleHolders[i].role, roleHolders[i].policyholder, roleHolders[i].quantity, roleHolders[i].expiration
158:      );
159:    }
diff --git a/src/LlamaPolicy.sol b/src/LlamaPolicy.sol
index 3fca63e..b46c68e 100644
--- a/src/LlamaPolicy.sol
+++ b/src/LlamaPolicy.sol
@@ -153,8 +153,9 @@ contract LlamaPolicy is ERC721NonTransferableMinimalProxy {
     }

     for (uint256 i = 0; i < roleHolders.length; i = LlamaUtils.uncheckedIncrement(i)) {
+      RoleHolderData calldata roleHolder = roleHolders[i];
       _setRoleHolder(
-        roleHolders[i].role, roleHolders[i].policyholder, roleHolders[i].quantity, roleHolders[i].expiration
+        roleHolder.role, roleHolder.policyholder, roleHolder.quantity, roleHolder.expiration
       );
     }

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaPolicy.sol#L161-L163

Gas Savings for LlamaPolicy.deploy, obtained via protocol's tests: Avg 708 gas

MedMaxAvg# calls
Before510115754064255015882412
After510115751169245015174412
File: src/LlamaPolicy.sol
161:    for (uint256 i = 0; i < rolePermissions.length; i = LlamaUtils.uncheckedIncrement(i)) {
162:      _setRolePermission(rolePermissions[i].role, rolePermissions[i].permissionId, rolePermissions[i].hasPermission);
163:    }
diff --git a/src/LlamaPolicy.sol b/src/LlamaPolicy.sol
index 3fca63e..c6df227 100644
--- a/src/LlamaPolicy.sol
+++ b/src/LlamaPolicy.sol
@@ -159,7 +159,8 @@ contract LlamaPolicy is ERC721NonTransferableMinimalProxy {
     }

     for (uint256 i = 0; i < rolePermissions.length; i = LlamaUtils.uncheckedIncrement(i)) {
-      _setRolePermission(rolePermissions[i].role, rolePermissions[i].permissionId, rolePermissions[i].hasPermission);
+      RolePermissionData calldata rolePermission = rolePermissions[i];
+      _setRolePermission(rolePermission.role, rolePermission.permissionId, rolePermission.hasPermission);
     }

Forgo internal function to save 1 STATICCALL

The _context internal function performs two external calls and returns both of the return values from those calls. Certain functions invoke _context but only use the return value from the first external call, thus performing an unnecessary extra external call. We can forgo using the internal function and instead only perform our desired external call to save 1 STATICCALL (100 gas).

Total Instances: 4

Estimated Gas Saved: 4 * 100 = 400

https://github.com/code-423n4/2023-06-llama/blob/main/src/llama-scripts/LlamaGovernanceScript.sol#L111-L115

Only perform address(this).LLAMA_CORE() to save 1 STATICCALL

File: src/llama-scripts/LlamaGovernanceScript.sol
111:  function createNewStrategiesAndSetRoleHolders(
112:    CreateStrategies calldata _createStrategies,
113:    RoleHolderData[] calldata _setRoleHolders
114:  ) external onlyDelegateCall {
115:    (LlamaCore core,) = _context(); // @audit: return value from `core.policy()` is not being used
diff --git a/src/llama-scripts/LlamaGovernanceScript.sol b/src/llama-scripts/LlamaGovernanceScript.sol
index 820872e..f886bf7 100644
--- a/src/llama-scripts/LlamaGovernanceScript.sol
+++ b/src/llama-scripts/LlamaGovernanceScript.sol
@@ -112,7 +112,7 @@ contract LlamaGovernanceScript is LlamaBaseScript {
     CreateStrategies calldata _createStrategies,
     RoleHolderData[] calldata _setRoleHolders
   ) external onlyDelegateCall {
-    (LlamaCore core,) = _context();
+    LlamaCore core = LlamaCore(LlamaExecutor(address(this)).LLAMA_CORE());
     core.createStrategies(_createStrategies.llamaStrategyLogic, _createStrategies.strategies);
     setRoleHolders(_setRoleHolders);
   }

https://github.com/code-423n4/2023-06-llama/blob/main/src/llama-scripts/LlamaGovernanceScript.sol#L120-L125

Only perform address(this).LLAMA_CORE() to save 1 STATICCALL

File: src/llama-scripts/LlamaGovernanceScript.sol
120:  function createNewStrategiesAndInitializeRolesAndSetRoleHolders(
121:    CreateStrategies calldata _createStrategies,
122:    RoleDescription[] calldata description,
123:    RoleHolderData[] calldata _setRoleHolders
124:  ) external onlyDelegateCall {
125:    (LlamaCore core,) = _context(); // @audit: return value from `core.policy()` is not being used
diff --git a/src/llama-scripts/LlamaGovernanceScript.sol b/src/llama-scripts/LlamaGovernanceScript.sol
index 820872e..f886bf7 100644
--- a/src/llama-scripts/LlamaGovernanceScript.sol
+++ b/src/llama-scripts/LlamaGovernanceScript.sol
@@ -122,7 +122,7 @@ contract LlamaGovernanceScript is LlamaBaseScript {
     RoleDescription[] calldata description,
     RoleHolderData[] calldata _setRoleHolders
   ) external onlyDelegateCall {
-    (LlamaCore core,) = _context();
+    LlamaCore core = LlamaCore(LlamaExecutor(address(this)).LLAMA_CORE());
     core.createStrategies(_createStrategies.llamaStrategyLogic, _createStrategies.strategies);
     initializeRoles(description);
     setRoleHolders(_setRoleHolders);

https://github.com/code-423n4/2023-06-llama/blob/main/src/llama-scripts/LlamaGovernanceScript.sol#L131-L135

Only perform address(this).LLAMA_CORE() to save 1 STATICCALL

File: src/llama-scripts/LlamaGovernanceScript.sol
131:  function createNewStrategiesAndSetRolePermissions(
132:    CreateStrategies calldata _createStrategies,
133:    RolePermissionData[] calldata _setRolePermissions
134:  ) external onlyDelegateCall {
135:    (LlamaCore core,) = _context(); // @audit: return value from `core.policy()` is not being used
diff --git a/src/llama-scripts/LlamaGovernanceScript.sol b/src/llama-scripts/LlamaGovernanceScript.sol
index 820872e..f886bf7 100644
--- a/src/llama-scripts/LlamaGovernanceScript.sol
+++ b/src/llama-scripts/LlamaGovernanceScript.sol
@@ -132,7 +132,7 @@ contract LlamaGovernanceScript is LlamaBaseScript {
     CreateStrategies calldata _createStrategies,
     RolePermissionData[] calldata _setRolePermissions
   ) external onlyDelegateCall {
-    (LlamaCore core,) = _context();
+    LlamaCore core = LlamaCore(LlamaExecutor(address(this)).LLAMA_CORE());
     core.createStrategies(_createStrategies.llamaStrategyLogic, _createStrategies.strategies);
     setRolePermissions(_setRolePermissions);
   }

https://github.com/code-423n4/2023-06-llama/blob/main/src/llama-scripts/LlamaGovernanceScript.sol#L140-L146

Only perform address(this).LLAMA_CORE() to save 1 STATICCALL

File: src/llama-scripts/LlamaGovernanceScript.sol
140:  function createNewStrategiesAndNewRolesAndSetRoleHoldersAndSetRolePermissions(
141:    CreateStrategies calldata _createStrategies,
142:    RoleDescription[] calldata description,
143:    RoleHolderData[] calldata _setRoleHolders,
144:    RolePermissionData[] calldata _setRolePermissions
145:  ) external onlyDelegateCall {
146:    (LlamaCore core,) = _context(); // @audit: return value from `core.policy()` is not being used
diff --git a/src/llama-scripts/LlamaGovernanceScript.sol b/src/llama-scripts/LlamaGovernanceScript.sol
index 820872e..f886bf7 100644
--- a/src/llama-scripts/LlamaGovernanceScript.sol
+++ b/src/llama-scripts/LlamaGovernanceScript.sol
@@ -143,7 +143,7 @@ contract LlamaGovernanceScript is LlamaBaseScript {
     RoleHolderData[] calldata _setRoleHolders,
     RolePermissionData[] calldata _setRolePermissions
   ) external onlyDelegateCall {
-    (LlamaCore core,) = _context();
+    LlamaCore core = LlamaCore(LlamaExecutor(address(this)).LLAMA_CORE());
     core.createStrategies(_createStrategies.llamaStrategyLogic, _createStrategies.strategies);
     initializeRoles(description);
     setRoleHolders(_setRoleHolders);

Multiple accesses of a mapping/array should use a local variable cache

Caching a mapping's value in a storage pointer when the value is accessed multiple times saves ~40 gas per access due to not having to perform the same offset calculation every time. Help the Optimizer by saving a storage variable's reference instead of repeatedly fetching it.

To achieve this, declare a storage pointer for the variable and use it instead of repeatedly fetching the reference in a map or an array. As an example, instead of repeatedly calling stakes[tokenId_], save its reference via a storage pointer: StakeInfo storage stakeInfo = stakes[tokenId_] and use the pointer instead.

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaPolicy.sol#L443-L448

Gas Savings for LlamaPolicy.revokePolicy, obtained via protocol's tests: Avg 116 gas

MedMaxAvg# calls
Before690401100675983711
After689161099125972111
File: src/LlamaPolicy.sol
443:    uint128 initialQuantity = roleBalanceCkpts[tokenId][role].latest();
444:    bool hadRole = initialQuantity > 0;
445:    bool willHaveRole = quantity > 0;
446:
447:    // Now we update the policyholder's role balance checkpoint.
448:    roleBalanceCkpts[tokenId][role].push(willHaveRole ? quantity : 0, expiration);
diff --git a/src/LlamaPolicy.sol b/src/LlamaPolicy.sol
index 3fca63e..7674061 100644
--- a/src/LlamaPolicy.sol
+++ b/src/LlamaPolicy.sol
@@ -440,12 +440,13 @@ contract LlamaPolicy is ERC721NonTransferableMinimalProxy {
     // checking if the quantity is nonzero, and we don't need to check the expiration when setting
     // the `hadRole` and `willHaveRole` variables.
     uint256 tokenId = _tokenId(policyholder);
-    uint128 initialQuantity = roleBalanceCkpts[tokenId][role].latest();
+    Checkpoints.History storage _roleBalanceCkpts = roleBalanceCkpts[tokenId][role];
+    uint128 initialQuantity = _roleBalanceCkpts.latest();
     bool hadRole = initialQuantity > 0;
     bool willHaveRole = quantity > 0;

     // Now we update the policyholder's role balance checkpoint.
-    roleBalanceCkpts[tokenId][role].push(willHaveRole ? quantity : 0, expiration);
+    _roleBalanceCkpts.push(willHaveRole ? quantity : 0, expiration);

     // If they don't hold a policy, we mint one for them. This means that even if you use 0 quantity
     // and 0 expiration, a policy is still minted even though they hold no roles. This is because

Refactor If/require statements to save SLOADs in case of early revert

Checks that involve calldata should come before checks that involve state variables, function calls, and calculations. By doing these checks first, the function is able to revert before using excessive gas in a call that may ultimately revert in an unhappy case.

Total Instances: 4

https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaAbsoluteQuorum.sol#L27-L35

The check in line 35 performs an SLOAD, while the check in lines 32-33 perform an external call and two SLOADs. We can move the check in line 35 above lines 32-33 to potentially save an SLOAD & External call in the unhappy path.

Note: This view function is called in the state mutating _createAction function in LlamaCore.sol

File: src/strategies/LlamaAbsoluteQuorum.sol
27:  function validateActionCreation(ActionInfo calldata /* actionInfo */ ) external view override {
28:    LlamaPolicy llamaPolicy = policy; // Reduce SLOADs.
29:    uint256 approvalPolicySupply = llamaPolicy.getRoleSupplyAsQuantitySum(approvalRole);
30:    if (approvalPolicySupply == 0) revert RoleHasZeroSupply(approvalRole);
31:
32:    uint256 disapprovalPolicySupply = llamaPolicy.getRoleSupplyAsQuantitySum(disapprovalRole); // @audit: 1 SLOAD + External call
33:    if (disapprovalPolicySupply == 0) revert RoleHasZeroSupply(disapprovalRole); // @audit: 1 SLOAD
34:
35:    if (minApprovals > approvalPolicySupply) revert InsufficientApprovalQuantity(); // @audit: 1 SLOAD
diff --git a/src/strategies/LlamaAbsoluteQuorum.sol b/src/strategies/LlamaAbsoluteQuorum.sol
index 66130c0..aee2ce3 100644
--- a/src/strategies/LlamaAbsoluteQuorum.sol
+++ b/src/strategies/LlamaAbsoluteQuorum.sol
@@ -29,10 +29,11 @@ contract LlamaAbsoluteQuorum is LlamaAbsoluteStrategyBase {
     uint256 approvalPolicySupply = llamaPolicy.getRoleSupplyAsQuantitySum(approvalRole);
     if (approvalPolicySupply == 0) revert RoleHasZeroSupply(approvalRole);

+    if (minApprovals > approvalPolicySupply) revert InsufficientApprovalQuantity();
+
     uint256 disapprovalPolicySupply = llamaPolicy.getRoleSupplyAsQuantitySum(disapprovalRole);
     if (disapprovalPolicySupply == 0) revert RoleHasZeroSupply(disapprovalRole);

-    if (minApprovals > approvalPolicySupply) revert InsufficientApprovalQuantity();
     if (minDisapprovals != type(uint128).max && minDisapprovals > disapprovalPolicySupply) {
       revert InsufficientDisapprovalQuantity();
     }

https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaAbsolutePeerReview.sol#L74-L82

The check in line 79 accesses storage, while the check in line 80 only accesses calldata. Move the check in line 80 above line 79 to potentially save an SLOAD in the unhappy path.

Note: This view function is called in the state mutating _preCastAssertions function in LlamaCore.sol

File: src/strategies/LlamaAbsolutePeerReview.sol
74:  function isDisapprovalEnabled(ActionInfo calldata actionInfo, address policyholder, uint8 role)
75:    external
76:    view
77:    override
78:  {
79:    if (minDisapprovals == type(uint128).max) revert DisapprovalDisabled(); // @audit: accesses storage
80:    if (actionInfo.creator == policyholder) revert ActionCreatorCannotCast(); // @audit: accesses calldata
81:    if (role != disapprovalRole && !forceDisapprovalRole[role]) revert InvalidRole(disapprovalRole);
82:  }
diff --git a/src/strategies/LlamaAbsolutePeerReview.sol b/src/strategies/LlamaAbsolutePeerReview.sol
index 85feb92..2df24ec 100644
--- a/src/strategies/LlamaAbsolutePeerReview.sol
+++ b/src/strategies/LlamaAbsolutePeerReview.sol
@@ -76,8 +76,9 @@ contract LlamaAbsolutePeerReview is LlamaAbsoluteStrategyBase {
     view
     override
   {
-    if (minDisapprovals == type(uint128).max) revert DisapprovalDisabled();
     if (actionInfo.creator == policyholder) revert ActionCreatorCannotCast();
+    if (minDisapprovals == type(uint128).max) revert DisapprovalDisabled();
     if (role != disapprovalRole && !forceDisapprovalRole[role]) revert InvalidRole(disapprovalRole);
   }
 }

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaPolicy.sol#L412-L418

The check in line 414 accesses storage, while the check in line 418 only accesses a stack variable. Move the check in line 418 above line 414 to potentially save 1 SLOAD on the unhappy path.

Note: This view function is called within state mutating functions in LlamaPolicy.sol.

File: src/LlamaPolicy.sol
412:  function _assertValidRoleHolderUpdate(uint8 role, uint128 quantity, uint64 expiration) internal view {
413:    // Ensure role is initialized.
414:    if (role > numRoles) revert RoleNotInitialized(role); // @audit: accesses storage
415:
416:    // Cannot set the ALL_HOLDERS_ROLE because this is handled in the _mint / _burn methods and can
417:    // create duplicate entries if set here.
418:    if (role == ALL_HOLDERS_ROLE) revert AllHoldersRole(); // @audit: accesses stack variable
diff --git a/src/LlamaPolicy.sol b/src/LlamaPolicy.sol
index 3fca63e..443e74c 100644
--- a/src/LlamaPolicy.sol
+++ b/src/LlamaPolicy.sol
@@ -410,13 +410,13 @@ contract LlamaPolicy is ERC721NonTransferableMinimalProxy {

   /// @dev Checks if the conditions are met for a `role` to be updated.
   function _assertValidRoleHolderUpdate(uint8 role, uint128 quantity, uint64 expiration) internal view {
-    // Ensure role is initialized.
-    if (role > numRoles) revert RoleNotInitialized(role);
-
     // Cannot set the ALL_HOLDERS_ROLE because this is handled in the _mint / _burn methods and can
     // create duplicate entries if set here.
     if (role == ALL_HOLDERS_ROLE) revert AllHoldersRole();

+    // Ensure role is initialized.
+    if (role > numRoles) revert RoleNotInitialized(role);
+
     // An expiration of zero is only allowed if the role is being removed. Roles are removed when
     // the quantity is zero. In other words, the relationships that are required between the role
     // quantity and expiration fields are:

https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaCore.sol#L317-L324

The check in line 324 accesses calldata, the check in line 323 accesses storage, and the check in lines 320-322 accesses storage at least once and potentially multiple times. To save at least one SLOAD in unhappy path, place the checks in the following order:

  1. Check in line 324
  2. Check in line 323
  3. Check in lines 320-322
File: src/LlamaCore.sol
317:  function executeAction(ActionInfo calldata actionInfo) external payable {
318:    // Initial checks that action is ready to execute.
319:    Action storage action = actions[actionInfo.id];
320:    ActionState currentState = getActionState(actionInfo); // @audit: accesses storage (at least 1 SLOAD, potentially more)
321:
322:    if (currentState != ActionState.Queued) revert InvalidActionState(currentState); // @audit: depends on line 320
323:    if (block.timestamp < action.minExecutionTime) revert MinExecutionTimeNotReached(); // @audit: accesses storage (1 SLOAD)
324:    if (msg.value != actionInfo.value) revert IncorrectMsgValue(); // @audit: accesses calldata
diff --git a/src/LlamaCore.sol b/src/LlamaCore.sol
index 89d60de..594e9f4 100644
--- a/src/LlamaCore.sol
+++ b/src/LlamaCore.sol
@@ -316,12 +316,13 @@ contract LlamaCore is Initializable {
   /// @param actionInfo Data required to create an action.
   function executeAction(ActionInfo calldata actionInfo) external payable {
     // Initial checks that action is ready to execute.
+    if (msg.value != actionInfo.value) revert IncorrectMsgValue();
+
     Action storage action = actions[actionInfo.id];
-    ActionState currentState = getActionState(actionInfo);
+    if (block.timestamp < action.minExecutionTime) revert MinExecutionTimeNotReached();

+    ActionState currentState = getActionState(actionInfo);
     if (currentState != ActionState.Queued) revert InvalidActionState(currentState);
-    if (block.timestamp < action.minExecutionTime) revert MinExecutionTimeNotReached();
-    if (msg.value != actionInfo.value) revert IncorrectMsgValue();

     action.executed = true;

#0 - c4-judge

2023-07-02T16:40:59Z

gzeon-c4 marked the issue as grade-b

#1 - 0xJCN

2023-07-06T13:21:09Z

Hello @gzeon-c4 ,

I would like to encourage you to revisit this issue. I believe it should receive a grade A and I will explain why down below. If you still decide to maintain the current grade after reading this, I would very much appreciate some advice on how I can improve my submisions and provide more value in the future.

  • As per my Analysis, I prioritized high-impact findings and exlcuded bot-like findings in an attempt to provide more value, and out of respect for the sponor's time.
  • A majority of the instances are benchmarked. The instances that are not benchmarked contain explanations via opcodes.
  • All instances contain diffs to illustrate the necessary refactoring needed for the findings.
  • All instances contain explanations.
  • All of the instances (with the exception of Issue 7) are high impact, meaning they save >= 100 gas.
  • This report only contains one finding Issue 7 that can be considered not high impact (This is debateable as it can only be high impact on an unhappy path).
  • This report contains no false findings.
  • The total amount of gas saved across all instances is displayed in the report.

In the spirit of fairness (as well as maintaining and encouraging high quality reports) I have provided a detailed comparison of the other reports which received a grade A. Some of these reports contain false findings/false positives, but a majority of the reports contain low impact findings akin to the kind in bot races. I believe reports such as these should be discouraged from being submitted as a regular gas report and instead be encouraged to participate in the bot races. Regular gas reports, in my opinion, should be held to a higher standard compared to bot races.

In addition, seeing as we are receiving compensation for our time and effort, I wanted to be fair to myself as it is difficult for me to observe the reports below and understand what I am 'missing' that resulted in my report being graded lower.

Issue #285

  • Note that none of the instances contain diffs to illustrate the neccessary refactoring needed. The warden takes the time to write their own test for each optimization, but since the code in each instance is unique I believe it would be more beneficial to the sponsor if the protocol's code was refactored and benchmarked.
  • G-01 low impact and would benefit from diffs.
  • G-02 low impact and would benefit from diffs.
  • G-03 This optimization only works for specific code and does not relate to every for loop. This optimization assumes a specific scenario where every for loop is writing to storage. In this case the optimization could work, but unfortunately this is not the case for the majority of the instances listed, if not all instances listed.
  • G-04 low impact.
  • G-05 bot-like. In addition, the link provided is from a repo which uses an outdated compiler version.
  • G-07 A large number of these instances were included in the bot race report.
  • G-08 low impact
  • G-09 low impact

Issue #230

  • Note that the instances do not include diffs to illustrate the necessary refactoring needed to make for the gas savings to take effect and this report includes no benchmarking.
  • G-01 low impact
  • G-02 no refactoring is given. I believe reafactorings are essential especially for optimizations that use assembly.
  • G-03 This finding lacks any explanation and refactoring.
  • G-04 This finding lacks refactoring and an explanation for how much gas would be saved.
  • G-05 low impact
  • G-06 This finding does not explain how much gas is saved.
  • G-07 This finding lacks refactoring
  • G-08 bot-like/low impact
  • G-10 No refactoring or explanation for gas saved is given.
  • G-11 low impact
  • G-12 This is false as this instance does not point to a storage array being read into memory. In this instance a new array is being created in memory. Using the storage keyword would result in a compiler error.
  • G-13 This is false as the two values mentioned are different and are being assigned to different variables.
  • G-14 This finding is context specific and the amount of gas savings could only be known via refactoring.
  • G-15 bot-like/low impact
  • G-16 Lacks refactoring and the amount of gas saved. Instantiating a stack variable inside of a for loop will result in the variable being discarded after the loop, but if it is instantiated outside of the loop then it will be left on the stack and this may result in unwanted stack manipulation for the remainder of the code.
  • G-17 This is false. The refactoring suggested results in the same number of storage slots as the original code. In the original code snippet 3 slots are used and 3 slots are also being used in the refactored code.
  • G-18 Low impact and this finding does not include refactoring or an explanation for the amount of gas saved.
  • G-19 Lacks refactoring or the amount of gas saved
  • G-20 Does not specify the amount of gas saved
  • G-21 Lacks refactoring or the amount of gas saved
  • G-23 This is the same as the finding above
  • G-24 Requires more explanation + proof that ERC721A can be used, how it can be used, and where it can specifically be used.
  • G-27 More QA.

Issue #227

  • Note that no findings contain diffs to illustrate the refactoring needed for the optimizations. In addition, no optimizations include benchmarking.
  • G-01 Lacks refactoring or the amount of gas saved
  • G-02 Lacks refactoring and the amount of gas saved. Instantiating a stack variable inside of a for loop will result in the variable being discarded after the loop, but if it is instantiated outside of the loop then it will be left on the stack and this may result in unwanted stack manipulation for the remainder of the code.
  • G-04 This is false. Public functions cost the same as external functions.
  • G-06 lacks refactoring
  • G-07 This is false as this instance does not point to a storage array being read into memory. In this instance a new array is being created in memory. Using the storage keyword would result in a compiler error.
  • G-08 Lacks refactoring or the amount of gas saved
  • G-09 Lacks refactoring or the amount of gas saved
  • G-10 bot-like/low impact
  • G-11 low impact
  • G-12 Lacks refactoring or the amount of gas saved. The link given refers to an outdated compiler version.
  • G-13 low impact
  • G-14 Lacks refactoring or the amount of gas saved
  • G-15 This is false as the two values mentioned are different and are being assigned to different variables.
  • G-16 low impact
  • G-17 This is false. The refactoring suggested results in the same number of storage slots as the original code. In the original code snippet 3 slots are used and 3 slots are also being used in the refactored code.
  • G-18 bot-like/low impact
  • G-19 Does not specify the amount of gas saved
  • G-20 This finding does not point to any specific instances.
  • G-21 Lacks refactoring and benchmarking for proof of optimization
  • G-22 This finding lacks any explanation and refactoring.
  • G-23 Low impact/bot-like
  • G-24 This finding is context specific and the amount of gas savings could only be known via refactoring.
  • G-25 More QA

Issue #117

  • Note that this report does not contain diffs to illustrate the neccessary changes needed for the optimizations nor does it contain benchmarks against the protocol's code. This report is also extremely similar to bot-race reports. Therefore, I will only comment on the high impact findings presented in this report.
  • G-11 This is false as the code snippet is taken out of context. There is an address state variable above the isFixedLengthApprovalPeriod state variable and therefore the refactoring suggested will not save 1 slot. The refactoring will result in 3 slots, similarly the original code also results in 3 slots
  • G-13 This finding does not point to any specific code and therefore we can not confirm the validity of the optimization.
  • G-15 This is false as the Config struct is not used in storage. It is used to instantiate a memory struct.

Issue #84

  • Note that this report contains multiple false findings and no benchmarks to illustrate the specific amount of gas saved for any instances.
  • G-01 This is false as the argument parameter in the instance given is already set to calldata
  • G-02 This is false as public functions cost the same as external functions.
  • G-03 This is false as the refactoring shown still does the same storage writes as the original code.
  • G-04 This is false as the contract given inherits from LlamaAbsoluteStrategyBase, which is upgradeable and thus the policy state variable is set in a separate initialize function instead of the constructor. Therefore, none of the state variables in LlamaAbsoluteStrategyBase, policy included, can be set as immutable.
  • G-05 This is false as the external function calls in question take different input arguments and are therefore unique and would have to be called separately. The refactorinng given would not mitigate any external calls.
  • G-06 This is false as LlamaAbsoluteStrategyBase is upgradeable and thus the policy state variable is set in a separate initialize function instead of the constructor. Therefore, none of the state variables in LlamaAbsoluteStrategyBase, policy included, can be set as immutable.
  • G-07 This is false as the external function calls in question take different input arguments and are therefore unique and would have to be called separately. The refactorinng given would not mitigate any external calls.
  • G-08 No benchmarks included as proof for the validity of the optimization.
  • G-09 No benchmarks included as proof for the validity of the optimization.
  • G-10 This finding does not explain how much gas would be saved. No benchmarks included.
  • G-11 This is false as grabbing block.timestamp uses the NUMBER opcode, which costs 2 Gas. Caching the timestamp in a variable will be more expensive because you have the perform stack manipulation (at least DUP (3 gas)) to use the variable.
  • G-12 This requires more information. Mappings are generally more efficient than arrays. Benchmarks should be included as proof for the validity of the optimization.
  • G-13 This is false as mappings can not be set to immutable. The suggested refactoring will result in a compiler error.
  • G-15 This finding is misleading as a majority of the loops in the contract in question already cache the length of the calldata array. In addition, the array is stored in calldata, not storage, as suggested in the explanation of the optimization.
  • G-16 This is false as public functions cost the same as external functions.
  • G-17 This is false as safeTransfer and safeTransferFrom does not explicitly check for a zero address. Checking for the zero address early and reverting early before any further calls or changes to state are made will save gas.
  • G-18 This would only have a small effect on deployment gas, not runtime. Gas savings are negligible.
  • G-19 This would only have a small effect on deployment gas, not runtime. Gas savings are negligible.
  • G-20 This is false as public functions cost the same as external functions.
  • G-21 This finding lacks any refactoring to illustrate the optimization.
  • G-22 low impact
  • G-23 This finding lacks refactoring and benchmarks as proof for the validity of the optimization
  • G-27 low impact
  • G-28 No links to specific code or refactoring is given

Issue #70

  • Note that none of the instances included benchmarks to illustrate the specific amount of gas that would be saved.
  • G-01 This finding does not include any explanation nor does it specify how much gas would be saved.
  • G-02 This finding does not include any benchmarks to specify how much gas is saved. In addition, the instance in question is a pure function, which do not cost gas.
  • G-03 This finding does not include any benchmarks to specify how much gas is saved. In addition, the instance in question is a pure function, which do not cost gas.
  • G-04 This finding only affects deployment gas, not runtime gas. This is also low impact.

#2 - gzeon-c4

2023-07-06T13:47:35Z

Thanks for flagging, will do another pass of gas report review.

#3 - c4-judge

2023-07-06T14:51:49Z

gzeon-c4 marked the issue as grade-a

#4 - 0xJCN

2023-07-06T21:37:52Z

Hi @gzeon-c4 ,

Thank you for taking the time to review my comment. I have one more clarifying question I'd like to ask you. I was wondering if you could offer some insight as to how this report could've been improved so that it qualified for "selected for report"? I'd appreciate any feedback so I can do better in the future and provide more value to the sponsor.

Thank you.

#5 - gzeon-c4

2023-07-08T11:28:31Z

Hi @gzeon-c4 ,

Thank you for taking the time to review my comment. I have one more clarifying question I'd like to ask you. I was wondering if you could offer some insight as to how this report could've been improved so that it qualified for "selected for report"? I'd appreciate any feedback so I can do better in the future and provide more value to the sponsor.

Thank you.

I actually got interrupted yesterday and will finish the review in the next few hours I think.

#6 - gzeon-c4

2023-07-08T12:25:00Z

Apologize again for the delay, I am going to mark this report as best because out of all the gas reports, this provided the most unique insight and the least inaccuracy. Judging qa and gas reports are tricky, since they are assigned a very small % of the overall prize pool it does not make a lot of sense to spend more time on it than HM issues. I usually quickly sort them into A B C bucket and reviewing only a few of them in more detail. That said, there are still ways to improve my process here as in retrospect I have been tricked by some warden to believe certain gas optimization is legit, when they are providing invalid or out-of-context examples.

Regarding this report, I think it looks good and it is impressive that everything is with actual coded poc and gas metering. It would be nicer if you can show the function you metered in the summary table (for example, a gas saving in a hot path is much better than in say init function which is only called once). Nit-picking but e.g. this finding:

Use already cached actionId to save 1 SLOAD

This seems to have this risk of making actionCount off due to possible reentrancy, it seems to be possible that an action is created during the external call to the strategy or the guard so if you use the cached action count you will only increment it by 1 even if many action is created. I am unsure this is an actual issue tho because the mapping will be overwritten anyway.

#7 - c4-judge

2023-07-08T12:25:05Z

gzeon-c4 marked the issue as selected for report

#8 - 0xJCN

2023-07-08T13:04:46Z

Apologize again for the delay, I am going to mark this report as best because out of all the gas reports, this provided the most unique insight and the least inaccuracy. Judging qa and gas reports are tricky, since they are assigned a very small % of the overall prize pool it does not make a lot of sense to spend more time on it than HM issues. I usually quickly sort them into A B C bucket and reviewing only a few of them in more detail. That said, there are still ways to improve my process here as in retrospect I have been tricked by some warden to believe certain gas optimization is legit, when they are providing invalid or out-of-context examples.

Regarding this report, I think it looks good and it is impressive that everything is with actual coded poc and gas metering. It would be nicer if you can show the function you metered in the summary table (for example, a gas saving in a hot path is much better than in say init function which is only called once). Nit-picking but e.g. this finding:

Use already cached actionId to save 1 SLOAD

This seems to have this risk of making actionCount off due to possible reentrancy, it seems to be possible that an action is created during the external call to the strategy or the guard so if you use the cached action count you will only increment it by 1 even if many action is created. I am unsure this is an actual issue tho because the mapping will be overwritten anyway.

Thank you for your detailed response. In future contests I will be sure to exclude init functions as I agree that they hold less value compared to functions that are expected to be called again and again. I will also be much more specific with the exact functions that I am metering. The action information seems to be stored into the mapping using the correct actionId. Th tests passed with this optimization, as I always make sure no obvious issues with the test suite occur, but I understand the observation.

I also understand that as a judge your time is rightfully spent evaluating HM for the majority of the judging process. As someone who primarily participates in Gas Optimization, I recognize this and therefore believe it is beneficial for me to share additional insight regarding some reports that seem to have inaccuracies, so we can provide more value to the sponsors. That being said I would like to provide some additional remarks regarding Issue 120 that I believe is important for the assessment of the report:

  • G-01 This finding is false. Firstly it is important to note that all 3 of the values in the struct are packed into one slot. Therefore only one Gcoldsload (2100 gas) will incur when the fields are accessed. The first instance (lines 106) will not save any gas when using the storage keyword. This is due to the fact that on line 107 all of the struct fields are read regardless. This means the same amount of SLOADs will take place regardless whether or not the struct is copied into memory. The second instance will not save any gas due to the fact that the timestamp field is read twice (once on line 135 and once on line 138). This means that the Gwarmaccess (100 gas) that would be saved by using the storage keyword (this comes from not reading the quantity field) is is ultimately spent when the timestamp field is read for a second time.
  • G-02 This finding is false as the ERC1155Data struct is not used in storage. It is used in calldata.
  • G-03 The 4th instance in this finding is false as the second occurance is a storage write and therefore the mapping's value can not be cached and used here.
AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter