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
Rank: 9/50
Findings: 3
Award: $730.13
π Selected for report: 0
π Solo Findings: 0
π Selected for report: Rolezn
Also found by: 0xSmartContract, DavidGiladi, QiuhaoLi, Sathish9098, kutugu, libratus, matrix_0wl, minhquanym
237.0469 USDC - $237.05
timelock
, but there is no timelock functionexecute
functioncreateAction()
function has no input controltimelock
, but there is no timelock functionIt is stated in the documents that the project uses timelock
, but there is no timelock function
README.md: 146: - Does it use a timelock function?: Yes
Centrality risk is high in the project, the role of 'onlyLlama' detailed below has very critical and important powers
Project and funds may be compromised by a malicious or stolen private key onlyLlama
msg.sender
32 results - 3 files src/LlamaCore.sol: 68: modifier onlyLlama() { 69: if (msg.sender != llamaExecutor) revert OnlyLlama(); 429: function createStrategies(ILlamaStrategy llamaStrategyLogic, bytes[] calldata strategyConfigs) external onlyLlama { 436: function createAccounts(ILlamaAccount llamaAccountLogic, bytes[] calldata accountConfigs) external onlyLlama { 444: function setGuard(address target, bytes4 selector, ILlamaActionGuard guard) external onlyLlama { 454: function authorizeScript(address script, bool authorized) external onlyLlama { src/LlamaPolicy.sol: 68: modifier onlyLlama() { 69: if (msg.sender != llamaExecutor) revert OnlyLlama(); 190: function initializeRole(RoleDescription description) external onlyLlama { 199: function setRoleHolder(uint8 role, address policyholder, uint128 quantity, uint64 expiration) external onlyLlama { 207: function setRolePermission(uint8 role, bytes32 permissionId, bool hasPermission) external onlyLlama { 222: function revokePolicy(address policyholder) external onlyLlama { 236: function updateRoleDescription(uint8 role, RoleDescription description) external onlyLlama { src/accounts/LlamaAccount.sol: 103: modifier onlyLlama() { 104: if (msg.sender != llamaExecutor) revert OnlyLlama(); 147: function transferNativeToken(NativeTokenData calldata nativeTokenData) public onlyLlama { 154: function batchTransferNativeToken(NativeTokenData[] calldata nativeTokenData) external onlyLlama { 165: function transferERC20(ERC20Data calldata erc20Data) public onlyLlama { 172: function batchTransferERC20(ERC20Data[] calldata erc20Data) external onlyLlama { 181: function approveERC20(ERC20Data calldata erc20Data) public onlyLlama { 187: function batchApproveERC20(ERC20Data[] calldata erc20Data) external onlyLlama { 198: function transferERC721(ERC721Data calldata erc721Data) public onlyLlama { 205: function batchTransferERC721(ERC721Data[] calldata erc721Data) external onlyLlama { 214: function approveERC721(ERC721Data calldata erc721Data) public onlyLlama { 220: function batchApproveERC721(ERC721Data[] calldata erc721Data) external onlyLlama { 229: function approveOperatorERC721(ERC721OperatorData calldata erc721OperatorData) public onlyLlama { 235: function batchApproveOperatorERC721(ERC721OperatorData[] calldata erc721OperatorData) external onlyLlama { 246: function transferERC1155(ERC1155Data calldata erc1155Data) external onlyLlama { 255: function batchTransferSingleERC1155(ERC1155BatchData calldata erc1155BatchData) public onlyLlama { 268: function batchTransferMultipleERC1155(ERC1155BatchData[] calldata erc1155BatchData) external onlyLlama { 277: function approveOperatorERC1155(ERC1155OperatorData calldata erc1155OperatorData) public onlyLlama { 283: function batchApproveOperatorERC1155(ERC1155OperatorData[] calldata erc1155OperatorData) external onlyLlama {
According to the scope information of the project, it is stated that it can be used in rollup chains and popular EVM chains.
README.md: 150: - Does it use rollups?: It should be able to be deployed on rollup chains 151: - Is it multi-chain?: It should be able to be deployed an any popular EVM chain
Some conventions in the project are set to Pragma ^0.8.19, allowing the conventions to be compiled with any 0.8.x compiler. The problem with this is that Arbitrum is Compatible with 0.8.20 and newer. Contracts compiled with these versions will result in a non-functional or potentially damaged version that does not behave as expected. The default behavior of the compiler will be to use the latest version which means it will compile with version 0.8.20 which will produce broken code by default.
execute
functionDescription: execute() function does not check the return value of the delegatecall or call functions.
src/LlamaExecutor.sol: 28 /// @return result The data returned by the function being called. 29: function execute(address target, uint256 value, bool isScript, bytes calldata data) 30: external 31: returns (bool success, bytes memory result) 32: { 33: if (msg.sender != LLAMA_CORE) revert OnlyLlamaCore(); 34: (success, result) = isScript ? target.delegatecall(data) : target.call{value: value}(data); 35: } 36: }
The success
control is made in the LlmamaCore.sol contract, but it is best practice to have this control directly in the function itself, not in the calling function;
src/LlamaCore.sol: 331 332: // Execute action. 333: (bool success, bytes memory result) = 334: executor.execute(actionInfo.target, actionInfo.value, action.isScript, actionInfo.data); 335: 336: if (!success) revert FailedActionExecution(result);
Recommendation: The return value will be checked if the code is corrected as below;
function execute(address target, uint256 value, bool isScript, bytes calldata data) external returns (bool success, bytes memory result) { if (msg.sender != LLAMA_CORE) revert OnlyLlamaCore(); if (isScript) { (success, result) = target.delegatecall(data); } else { (success, result) = target.call{value: value}(data); } require(success, "Call failed"); }
createAction()
function has no input controlsrc/LlamaCore.sol: 248 249: /// @notice Creates an action. The creator needs to hold a policy with the permission ID of the provided 250: /// `(target, selector, strategy)`. 251: /// @dev Use `""` for `description` if there is no description. 252: /// @param role The role that will be used to determine the permission ID of the policyholder. 253: /// @param strategy The strategy contract that will determine how the action is executed. 254: /// @param target The contract called when the action is executed. 255: /// @param value The value in wei to be sent when the action is executed. 256: /// @param data Data to be called on the target when the action is executed. 257: /// @param description A human readable description of the action and the changes it will enact. 258: /// @return actionId Action ID of the newly created action. 259: function createAction( 260: uint8 role, 261: ILlamaStrategy strategy, 262: address target, 263: uint256 value, 264: bytes calldata data, 265: string memory description 266: ) external returns (uint256 actionId) { + if (bytes(description).length == 0) { + description = ""; // Set description to empty string if it is not provided + } 267: actionId = _createAction(msg.sender, role, strategy, target, value, data, description); 268: }
The &&
condition searches for two conditions;
1- address(factory).code.length > 0
2- !factory.authorizedStrategyLogics(llamaStrategyLogic))
If the !factory.authorizedStrategyLogics(llamaStrategyLogic) condition is met, the 'address(factory).code.length > 0' condition will already be fulfilled, so it is unnecessary
src/LlamaCore.sol: 628 { - 629: if (address(factory).code.length > 0 && !factory.authorizedStrategyLogics(llamaStrategyLogic)) { + if (!factory.authorizedStrategyLogics(llamaStrategyLogic)) { 630: // The only edge case where this check is skipped is if `_deployStrategies()` is called by root llama instance 631: // during Llama Factory construction. This is because there is no code at the Llama Factory address yet. 632: revert UnauthorizedStrategyLogic(); 633: }
At the start of the project, the system may need to be stopped or upgraded, I suggest you have a script beforehand and add it to the documentation. This can also be called an " EMERGENCY STOP (CIRCUIT BREAKER) PATTERN ".
https://github.com/maxwoe/solidity_patterns/blob/master/security/EmergencyStop.sol
Context:
12 results - 8 files src/LlamaCore.sol: 223 /// @return bootstrapPermissionId The permission ID that's used to set role permissions. 224: function initialize( 225 string memory _name, src/LlamaPolicy.sol: 142 /// @param rolePermissions The `role`, `permissionId` and whether the role has the permission of the role permissions. 143: function initialize( 144 string calldata _name, 189 /// @notice Initializes a new role with the given role ID and description 190: function initializeRole(RoleDescription description) external onlyLlama { 191 _initializeRole(description); src/accounts/LlamaAccount.sol: 129 /// @param config Llama account initialization configuration. 130: function initialize(bytes memory config) external initializer { 131 llamaExecutor = address(LlamaCore(msg.sender).executor()); src/interfaces/ILlamaAccount.sol: 17 /// different account logic contracts. 18: function initialize(bytes memory config) external; 19 } src/interfaces/ILlamaStrategy.sol: 28 /// different strategies. 29: function initialize(bytes memory config) external; 30 src/llama-scripts/LlamaGovernanceScript.sol: 84 85: function initializeRolesAndSetRoleHolders( 86 RoleDescription[] calldata description, 92 93: function initializeRolesAndSetRolePermissions( 94 RoleDescription[] calldata description, 100 101: function initializeRolesAndSetRoleHoldersAndSetRolePermissions( 102 RoleDescription[] calldata description, 174 175: function initializeRoles(RoleDescription[] calldata description) public onlyDelegateCall { 176 (, LlamaPolicy policy) = _context(); src/strategies/LlamaAbsoluteStrategyBase.sol: 152 /// @inheritdoc ILlamaStrategy 153: function initialize(bytes memory config) external virtual initializer { 154 Config memory strategyConfig = abi.decode(config, (Config)); src/strategies/LlamaRelativeQuorum.sol: 155 /// @inheritdoc ILlamaStrategy 156: function initialize(bytes memory config) external initializer { 157 Config memory strategyConfig = abi.decode(config, (Config));
Description: Events help non-contract tools to track changes, and events prevent users from being surprised by changes Issuing event-emit during initialization is a detail that many projects skip
Recommendation: Add Event-Emit
Some imports arenβt used inside the project
src/strategies/LlamaAbsolutePeerReview.sol: 4: import {Initializable} from "@openzeppelin/proxy/utils/Initializable.sol"; 6: import {FixedPointMathLib} from "@solmate/utils/FixedPointMathLib.sol"; 10: import {ActionState} from "src/lib/Enums.sol"; 11: import {LlamaUtils} from "src/lib/LlamaUtils.sol";
Since this is a low level language that is more difficult to parse by readers, include extensive documentation, comments on the rationale behind its use, clearly explaining what each assembly instruction does
This will make it easier for users to trust the code, for reviewers to validate the code, and for developers to build on or update the code.
Note that using Aseembly removes several important security features of Solidity, which can make the code more insecure and more error-prone.
3 results - 2 files src/accounts/LlamaAccount.sol: 338 function _readSlot0() internal view returns (bytes32 slot0) { 339: assembly { 340 slot0 := sload(0) src/lib/Checkpoints.sol: 204 { 205: assembly { 206 mstore(0, self.slot) 223 function sqrt(uint256 x) internal pure returns (uint256 z) { 224: assembly { 225 let y := x // We start y at x, which will help us make our initial estimate.
#0 - c4-pre-sort
2023-06-18T20:48:47Z
0xSorryNotSorry marked the issue as high quality report
#1 - c4-judge
2023-07-02T16:12:21Z
gzeon-c4 marked the issue as grade-b
#2 - c4-judge
2023-07-02T16:22:01Z
gzeon-c4 marked the issue as grade-a
#3 - c4-judge
2023-07-02T16:25:47Z
gzeon-c4 marked the issue as selected for report
#4 - c4-judge
2023-07-02T16:26:24Z
gzeon-c4 marked the issue as not selected for report
π Selected for report: JCN
Also found by: 0xSmartContract, DavidGiladi, Rageur, Raihan, Rolezn, SAAJ, SAQ, SM3_SS, Sathish9098, VictoryGod, hunter_w3b, lsaudit, naman1778, petrichor, sebghatullah, shamsulhaq123
23.8054 USDC - $23.81
code.length
checkassembly
to write address storage values>=
costs less gas than >
The execute() function can be optimized by splitting it into two functions: executeWithScript() and executeWithNotScript()
Such a use provides gas optimization as a result of the elimination of unnecessary controls. In addition, code readability will be ensured.
src\LlamaExecutor.sol: 29: function execute(address target, uint256 value, bool isScript, bytes calldata data) 30: external 31: returns (bool success, bytes memory result) 32: { 33: if (msg.sender != LLAMA_CORE) revert OnlyLlamaCore(); 34: (success, result) = isScript ? target.delegatecall(data) : target.call{value: value}(data); 35: }
https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaExecutor.sol#L29-L35
src\LlamaExecutor.sol: - 29: function execute(address target, uint256 value, bool isScript, bytes calldata data) - 30: external - 31: returns (bool success, bytes memory result) - 32: { - 33: if (msg.sender != LLAMA_CORE) revert OnlyLlamaCore(); - 34: (success, result) = isScript ? target.delegatecall(data) : target.call{value: value}(data); - 35: } + function executeWithScript(address _target, bytes calldata _data) + external + returns (bool success, bytes memory result) + { + if (msg.sender != LLAMA_CORE) revert OnlyLlamaCore(); + (success, result) = _target.delegatecall(_data); + } + function executeWithNotScript(address target, uint256 value, bytes calldata data) + external + returns (bool success, bytes memory result) + { + if (msg.sender != LLAMA_CORE) revert OnlyLlamaCore(); + (success, result) = target.call{value: value}(data); + }
code.length
checksrc\LlamaFactory.sol: 86: mapping(ILlamaStrategy => bool) public authorizedStrategyLogics; 183: function authorizeStrategyLogic(ILlamaStrategy strategyLogic) external onlyRootLlama { 184: _authorizeStrategyLogic(strategyLogic); 185: } 267: function _authorizeStrategyLogic(ILlamaStrategy strategyLogic) internal { 268: authorizedStrategyLogics[strategyLogic] = true; 269: emit StrategyLogicAuthorized(strategyLogic); 270: }
In case of achievable mapping, code.length
is greater than zero.
In this case, removing address(factory).code.length > 0
in the _deployStrategies() function will provide gas optimization.
src\LlamaCore.sol: 625: function _deployStrategies(ILlamaStrategy llamaStrategyLogic, bytes[] calldata strategyConfigs) 626: internal 627: returns (ILlamaStrategy firstStrategy) 628: { 629: if (address(factory).code.length > 0 && !factory.authorizedStrategyLogics(llamaStrategyLogic)) { 630: // The only edge case where this check is skipped is if `_deployStrategies()` is called by root llama instance 631: // during Llama Factory construction. This is because there is no code at the Llama Factory address yet. 632: revert UnauthorizedStrategyLogic(); 633: }
https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaCore.sol#L629
Recommendation Code:
src\LlamaCore.sol: 625: function _deployStrategies(ILlamaStrategy llamaStrategyLogic, bytes[] calldata strategyConfigs) 626: internal 627: returns (ILlamaStrategy firstStrategy) 628: { - 629: if (address(factory).code.length > 0 && !factory.authorizedStrategyLogics(llamaStrategyLogic)) { + 629: if (!factory.authorizedStrategyLogics(llamaStrategyLogic)) { 630: // The only edge case where this check is skipped is if `_deployStrategies()` is called by root llama instance 631: // during Llama Factory construction. This is because there is no code at the Llama Factory address yet. 632: revert UnauthorizedStrategyLogic(); 633: }
Each iteration of the cycle requires a gas flow. There may come a time when more gas than allocated is required to save a block. In this case, all iterations of the loop will fail.
There are 14 examples of the topic. The suggestion block is made for only one function.
8 results - 1 file:
src\accounts\LlamaAccount.sol: 154: function batchTransferNativeToken(NativeTokenData[] calldata nativeTokenData) external onlyLlama { 155: uint256 length = nativeTokenData.length; 156: for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { 157: transferNativeToken(nativeTokenData[i]); 158: } 159: } 172: function batchTransferERC20(ERC20Data[] calldata erc20Data) external onlyLlama { 173: uint256 length = erc20Data.length; 174: for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { 175: transferERC20(erc20Data[i]); 176: } 177: } 187: function batchApproveERC20(ERC20Data[] calldata erc20Data) external onlyLlama { 188: uint256 length = erc20Data.length; 189: for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { 190: approveERC20(erc20Data[i]); 191: } 192: } 205: function batchTransferERC721(ERC721Data[] calldata erc721Data) external onlyLlama { 206: uint256 length = erc721Data.length; 207: for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { 208: transferERC721(erc721Data[i]); 209: } 210: } 220: function batchApproveERC721(ERC721Data[] calldata erc721Data) external onlyLlama { 221: uint256 length = erc721Data.length; 222: for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { 223: approveERC721(erc721Data[i]); 224: } 225: } 235: function batchApproveOperatorERC721(ERC721OperatorData[] calldata erc721OperatorData) external onlyLlama { 236: uint256 length = erc721OperatorData.length; 237: for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { 238: approveOperatorERC721(erc721OperatorData[i]); 239: } 240: } 268: function batchTransferMultipleERC1155(ERC1155BatchData[] calldata erc1155BatchData) external onlyLlama { 269: uint256 length = erc1155BatchData.length; 270: for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { 271: batchTransferSingleERC1155(erc1155BatchData[i]); 272: } 273: } 283: function batchApproveOperatorERC1155(ERC1155OperatorData[] calldata erc1155OperatorData) external onlyLlama { 284: uint256 length = erc1155OperatorData.length; 285: for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { 286: approveOperatorERC1155(erc1155OperatorData[i]); 287: } 288: }
https://github.com/code-423n4/2023-06-llama/blob/main/src/accounts/LlamaAccount.sol#L154-L159 https://github.com/code-423n4/2023-06-llama/blob/main/src/accounts/LlamaAccount.sol#L172-L177 https://github.com/code-423n4/2023-06-llama/blob/main/src/accounts/LlamaAccount.sol#L187-L192 https://github.com/code-423n4/2023-06-llama/blob/main/src/accounts/LlamaAccount.sol#L205-L210 https://github.com/code-423n4/2023-06-llama/blob/main/src/accounts/LlamaAccount.sol#L220-L225 https://github.com/code-423n4/2023-06-llama/blob/main/src/accounts/LlamaAccount.sol#L235-L240 https://github.com/code-423n4/2023-06-llama/blob/main/src/accounts/LlamaAccount.sol#L268-L273 https://github.com/code-423n4/2023-06-llama/blob/main/src/accounts/LlamaAccount.sol#L283-L288
Recommendation Code:
src\accounts\LlamaAccount.sol: + uint256 private constant ERC1155_OPERATOR_DATA_LENGTH = 333; 283: function batchApproveOperatorERC1155(ERC1155OperatorData[] calldata erc1155OperatorData) external onlyLlama { 284: uint256 length = erc1155OperatorData.length; + if (erc1155OperatorData > ERC1155_OPERATOR_DATA_LENGTH) revert maxLength(); 285: for (uint256 i = 0; i < length; i = LlamaUtils.uncheckedIncrement(i)) { 286: approveOperatorERC1155(erc1155OperatorData[i]); 287: } 288: }
Placing events inside loops results in the event being emitted at each iteration, which can significantly increase gas consumption. By moving events outside the loop and emitting them globally, unnecessary gas expenses can be minimized, leading to more efficient and cost-effective contract execution.
2 results - 1 file:
src\LlamaCore.sol: 636: for (uint256 i = 0; i < strategyLength; i = LlamaUtils.uncheckedIncrement(i)) { 637: bytes32 salt = bytes32(keccak256(strategyConfigs[i])); 638: ILlamaStrategy strategy = ILlamaStrategy(Clones.cloneDeterministic(address(llamaStrategyLogic), salt)); 639: strategy.initialize(strategyConfigs[i]); 640: strategies[strategy] = true; 641: emit StrategyCreated(strategy, llamaStrategyLogic, strategyConfigs[i]); 642: if (i == 0) firstStrategy = strategy; 643: } 656: for (uint256 i = 0; i < accountLength; i = LlamaUtils.uncheckedIncrement(i)) { 657: bytes32 salt = bytes32(keccak256(accountConfigs[i])); 658: ILlamaAccount account = ILlamaAccount(Clones.cloneDeterministic(address(llamaAccountLogic), salt)); 659: account.initialize(accountConfigs[i]); 660: emit AccountCreated(account, llamaAccountLogic, accountConfigs[i]); 661: }
https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaCore.sol#L641 https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaCore.sol#L660
assembly
to write address storage values5 results - 4 files:
src\LlamaCore.sol: 232: factory = LlamaFactory(msg.sender); 235: policy = _policy;
https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaCore.sol#L232 https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaCore.sol#L235
src\strategies\LlamaAbsoluteStrategyBase.sol: 155: llamaCore = LlamaCore(msg.sender);
src\strategies\LlamaRelativeQuorum.sol: 158: llamaCore = LlamaCore(msg.sender);
https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaRelativeQuorum.sol#L158
src\LlamaPolicy.sol: 150: factory = LlamaFactory(msg.sender); 183: llamaExecutor = _llamaExecutor;
https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaPolicy.sol#L150 https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaPolicy.sol#L183
Using nested is cheaper than using && multiple check combinations. There are more advantages, such as easier to read code and better coverage reports.
17 results - 7 files:
src\LlamaCore.sol: 629: if (address(factory).code.length > 0 && !factory.authorizedStrategyLogics(llamaStrategyLogic)) { 649: if (address(factory).code.length > 0 && !factory.authorizedAccountLogics(llamaAccountLogic)) {
https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaCore.sol#L629 https://github.com/code-423n4/2023-06-llama/blob/main/src/LlamaCore.sol#L629
src\LlamaPolicyMetadataParamRegistry.sol: 20: if ( 21: msg.sender != address(llamaExecutor) && msg.sender != address(ROOT_LLAMA_EXECUTOR) && msg.sender != LLAMA_FACTORY 22: ) revert UnauthorizedCaller();
src\llama-scripts\LlamaGovernanceScript.sol: 74: if (!addressIsCore && !addressIsPolicy) revert UnauthorizedTarget(targets[i]);
src\strategies\LlamaAbsolutePeerReview.sol: 56: if ( 57: minDisapprovals != type(uint128).max 58: && minDisapprovals > disapprovalPolicySupply - actionCreatorDisapprovalRoleQty 59: ) revert InsufficientDisapprovalQuantity(); 68: if (role != approvalRole && !forceApprovalRole[role]) revert InvalidRole(approvalRole); 81: if (role != disapprovalRole && !forceDisapprovalRole[role]) revert InvalidRole(disapprovalRole);
https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaAbsolutePeerReview.sol#L56-L59 https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaAbsolutePeerReview.sol#L68 https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaAbsolutePeerReview.sol#L81
src\strategies\LlamaAbsoluteQuorum.sol: 36: if (minDisapprovals != type(uint128).max && minDisapprovals > disapprovalPolicySupply) { 49: if (role != approvalRole && !forceApprovalRole[role]) revert InvalidRole(approvalRole); 61: if (role != disapprovalRole && !forceDisapprovalRole[role]) revert InvalidRole(disapprovalRole);
https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaAbsoluteQuorum.sol#L36 https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaAbsoluteQuorum.sol#L49 https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaAbsoluteQuorum.sol#L61
src\strategies\LlamaAbsoluteStrategyBase.sol: 213: if (role != approvalRole && !forceApprovalRole[role]) return 0; 230: if (role != disapprovalRole && !forceDisapprovalRole[role]) return 0;
https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaAbsoluteStrategyBase.sol#L213 https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaAbsoluteStrategyBase.sol#L230
src\strategies\LlamaRelativeQuorum.sol: 216: if (role != approvalRole && !forceApprovalRole[role]) revert InvalidRole(approvalRole); 221: if (role != approvalRole && !forceApprovalRole[role]) return 0; 231: if (role != disapprovalRole && !forceDisapprovalRole[role]) revert InvalidRole(disapprovalRole); 240: if (role != disapprovalRole && !forceDisapprovalRole[role]) return 0;
https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaRelativeQuorum.sol#L216 https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaRelativeQuorum.sol#L221 https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaRelativeQuorum.sol#L231 https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaRelativeQuorum.sol#L240
Recomendation Code:
src\strategies\LlamaRelativeQuorum.sol: - 216: if (role != approvalRole && !forceApprovalRole[role]) revert InvalidRole(approvalRole); + 216: if (role != approvalRole) { + if (!forceApprovalRole[role]) { + revert InvalidRole(approvalRole); + } + }
>=
costs less gas than >
The compiler uses opcodes GT and ISZERO for solidity code that uses >, but only requires LT for >=, which saves 3 gas
2 results - 2 files:
src\strategies\LlamaAbsoluteStrategyBase.sol: 215: return quantity > 0 && forceApprovalRole[role] ? type(uint128).max : quantity; 232: return quantity > 0 && forceDisapprovalRole[role] ? type(uint128).max : quantity;
https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaAbsoluteStrategyBase.sol#L215 https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaAbsoluteStrategyBase.sol#L232
src\strategies\LlamaRelativeQuorum.sol: 223: return quantity > 0 && forceApprovalRole[role] ? type(uint128).max : quantity; 242: return quantity > 0 && forceDisapprovalRole[role] ? type(uint128).max : quantity;
https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaRelativeQuorum.sol#L223 https://github.com/code-423n4/2023-06-llama/blob/main/src/strategies/LlamaRelativeQuorum.sol#L242
#0 - c4-pre-sort
2023-06-18T21:09:59Z
0xSorryNotSorry marked the issue as high quality report
#1 - c4-judge
2023-07-02T16:43:43Z
gzeon-c4 marked the issue as grade-b
#2 - c4-judge
2023-07-06T15:13:47Z
gzeon-c4 marked the issue as grade-a
#3 - c4-judge
2023-07-08T11:36:01Z
gzeon-c4 marked the issue as grade-b
π Selected for report: 0xnev
Also found by: 0xSmartContract, K42, QiuhaoLi, VictoryGod, dirk_y, joestakey, ktg, kutugu, libratus, mahdirostami, neko_nyaa, peanuts, xuwinnie
469.2665 USDC - $469.27
List | Head | Details |
---|---|---|
a) | The approach I followed when reviewing the code | Stages in my code review and analysis |
b) | Analysis of the code base | What is unique? How are the existing patterns used? |
c) | Test analysis | Test scope of the project and quality of tests |
d) | Architectural | Architecture feedback |
e) | Documents | What is the scope and quality of documentation for Users and Administrators? |
f) | Centralization risks | How was the risk of centralization handled in the project, what could be alternatives? |
g) | Systemic risks | Potential systemic risks in the project |
h) | Competition analysis | What are similar projects? |
i) | Security Approach of the Project | Audit approach of the Project |
j) | Other Audit Reports and Automated Findings | What are the previous Audit reports and their analysis |
k) | Gas Optimization | Gas usage approach of the project and alternative solutions to it |
l) | Project team | Details about the project team and approach |
m) | Other recommendations | What is unique? How are the existing patterns used? |
n) | New insights and learning from this audit | Things learned from the project |
o) | Resources - Frameworks | Details of the sources, documents and frameworks used in the above analysis |
p) | Time spent on analysis | Time detail of individual stages in my code review and analysis |
First, by examining the scope of the code, I determined my code review and analysis strategy. https://github.com/code-423n4/2023-06-llama/tree/main#scoping-details
Accordingly, I analyzed and audited the subject in the following steps;
Number | Stage | Details | Information |
---|---|---|---|
1 | Compile and Run Test | Installation | Test and installation structure is simple, cleanly designed |
2 | Architecture Review | The video explainer provides a high-level overview of the Llama system and the docs describe the core components. | Provides a basic architectural teaching for General Architecture |
3 | Graphical Analysis | Graphical Analysis with Solidity-metrics | A visual view has been made to dominate the general structure of the codes of the project. |
4 | Slither Analysis | Slither Report | The Slither output did not indicate a direct security risk to us, but at some points it did give some insight into the project's careful scrutiny. These ; 1) Re-entrancy risks 2) Pragma version0.8.19 necessitates a version too recent to be trusted. |
5 | Test Suits | Tests | In this section, the scope and content of the tests of the project are analyzed. |
6 | Manuel Code Review | Scope | According to the architectural design, the codes were examined starting from the factory contract, which is the main starting point. |
6 | Project Spearbit Audit | Spearbit | According to the project team on the Discord channel, all the problems related to this audit have been fixed, this report gives us a clue about the topics that should be considered in the current audit (Ex: Access Control) |
Llama is a governance system for onchain organizations. It uses non-transferable NFTs to encode access control, features programmatic control of funds, and includes a modular framework to define action execution rules.
Code architecture ; It starts in a structure that produces instances with the Factory and clone structure that we see in many projects.
Distribution of Llama instances has been examined by Factory. LlamaFactory
Deploys a new Llama instance .Factory contract using OpenZeppelin's "Clones.sol" library. Llama examples are in Minimal Proxy structure.
The deploy()
function is the most important review function and coordinates the deployment, especially the access control privileges are checked
It is the LlamaCore.sol contract used by Factory for distribution of Llama instances. This contract is central to all projects, therefore its interaction with all contracts is critical and the usual suspect from a security point of view.
Since the LlamaCore contract is a proxy contract, security queries were made in the "Initializable" structure (For example; Openzeppelin "Initializable.sol" was imported, imported with the is Initializable
keyword, double execution was prevented with _disableInitializers()
in the constructor.
The function executeAction() interacts with the LlamaExecutor.sol
contract by external call and It runs the execute()
function there, the reason for this is explained in the project as follows;
Using a separate executor contract ensures `target` being delegatecalled cannot write to `LlamaCore`'s storage. By using a sole executor for calls and delegatecalls, a Llama instance is represented by one contract address.
The LlamaCore
contract uses the EIP712 standard, we confirm whether it uses this standard correctly eip-712
The alternative in this section is the use of eip-5267; eip-5267
LlamaPolicy
An ERC721 contract where each token is non-transferable, functions as the respective policy for a given policyholder and has roles assigned to create
, approve
and disapprove
actions.
What makes this contract different; It is a non-transferable token management. The focus should be on security in this section.
Other contracts were examined in the final stage.
There is a comprehensive test kit with many tests including fuzzing tests using Foundry Test coverage is 90% and content quality is above average
The design of the project is based on a structure that produces minimal proxy clone with Factory, this structure is frequently used since Uniswapv2. Llama Core Instances cloned by the Factory contract are the base contract and provide project authorizations and flow
The script structure is the original part of the project, it is not a structure that is seen mostly, so the focus should be on the code control here.
Innovative proposal to the project: Instead of the Factory + Instance structure, the only contract structure used in Uniswapv4 can be used, this is a radical solution that will reduce gas costs by 90% at the same time: βsingletonβ contracts from Uniswapv4
https://blog.uniswap.org/uniswap-v4#what-is-uniswap-v4
It is recommended to increase the documents in terms of auditability of the project, only the general structure is explained. https://github.com/code-423n4/2023-06-llama/tree/main/docs
It is stated in the documents that the project uses timelock
, but there is no timelock function.
README.md: 146: - Does it use a timelock function?: Yes
The philosophy of the project is not stated in the documents, the main philosophy stated in the figure below can help this;
Philosophy of the Project: In order to understand the Philosophy of the Project, it is necessary to start by looking at the differences between traditional governance and on-chain governance.
Centralization vs. decentralization: Traditional governance is typically centralized, meaning that there is a single entity or group of entities that holds power and makes decisions. On-chain governance, on the other hand, is decentralized, meaning that power is distributed among all participants in the network. Transparency vs. opacity: Traditional governance is often opaque, meaning that it is difficult or impossible for stakeholders to know how decisions are being made. On-chain governance, on the other hand, is transparent, meaning that all decisions are made on the blockchain and can be viewed by anyone. Speed vs. slowness: Traditional governance can be slow, as it often requires a lot of deliberation and consensus-building. On-chain governance, on the other hand, can be much faster, as decisions can be made quickly and easily through voting. Cost vs. free: Traditional governance can be expensive, as it often requires the hiring of lawyers, consultants, and other experts. On-chain governance, on the other hand, is free, as it is all done through the blockchain.
Overall, on-chain governance is a more transparent, decentralized, and efficient way to make decisions. However, it is important to note that on-chain governance is still a relatively new concept, and there are still some challenges that need to be addressed. For example, it can be difficult to get a quorum of participants to vote on proposals, and there is always the risk of malicious actors manipulating the voting process.
Despite these challenges, on-chain governance has the potential to revolutionize the way that organizations are governed. It can help to make organizations more transparent, accountable, and efficient. As the technology continues to develop, on-chain governance is likely to become more widely adopted.
A single point of failure is not acceptable for this project Centrality risk is high in the project, the role of 'onlyLlama' detailed below has very critical and important powers
Project and funds may be compromised by a malicious or stolen private key onlyLlama
msg.sender
32 results - 3 files src/LlamaCore.sol: 68: modifier onlyLlama() { 69: if (msg.sender != llamaExecutor) revert OnlyLlama(); 429: function createStrategies(ILlamaStrategy llamaStrategyLogic, bytes[] calldata strategyConfigs) external onlyLlama { 436: function createAccounts(ILlamaAccount llamaAccountLogic, bytes[] calldata accountConfigs) external onlyLlama { 444: function setGuard(address target, bytes4 selector, ILlamaActionGuard guard) external onlyLlama { 454: function authorizeScript(address script, bool authorized) external onlyLlama { src/LlamaPolicy.sol: 68: modifier onlyLlama() { 69: if (msg.sender != llamaExecutor) revert OnlyLlama(); 190: function initializeRole(RoleDescription description) external onlyLlama { 199: function setRoleHolder(uint8 role, address policyholder, uint128 quantity, uint64 expiration) external onlyLlama { 207: function setRolePermission(uint8 role, bytes32 permissionId, bool hasPermission) external onlyLlama { 222: function revokePolicy(address policyholder) external onlyLlama { 236: function updateRoleDescription(uint8 role, RoleDescription description) external onlyLlama { src/accounts/LlamaAccount.sol: 103: modifier onlyLlama() { 104: if (msg.sender != llamaExecutor) revert OnlyLlama(); 147: function transferNativeToken(NativeTokenData calldata nativeTokenData) public onlyLlama { 154: function batchTransferNativeToken(NativeTokenData[] calldata nativeTokenData) external onlyLlama { 165: function transferERC20(ERC20Data calldata erc20Data) public onlyLlama { 172: function batchTransferERC20(ERC20Data[] calldata erc20Data) external onlyLlama { 181: function approveERC20(ERC20Data calldata erc20Data) public onlyLlama { 187: function batchApproveERC20(ERC20Data[] calldata erc20Data) external onlyLlama { 198: function transferERC721(ERC721Data calldata erc721Data) public onlyLlama { 205: function batchTransferERC721(ERC721Data[] calldata erc721Data) external onlyLlama { 214: function approveERC721(ERC721Data calldata erc721Data) public onlyLlama { 220: function batchApproveERC721(ERC721Data[] calldata erc721Data) external onlyLlama { 229: function approveOperatorERC721(ERC721OperatorData calldata erc721OperatorData) public onlyLlama { 235: function batchApproveOperatorERC721(ERC721OperatorData[] calldata erc721OperatorData) external onlyLlama { 246: function transferERC1155(ERC1155Data calldata erc1155Data) external onlyLlama { 255: function batchTransferSingleERC1155(ERC1155BatchData calldata erc1155BatchData) public onlyLlama { 268: function batchTransferMultipleERC1155(ERC1155BatchData[] calldata erc1155BatchData) external onlyLlama { 277: function approveOperatorERC1155(ERC1155OperatorData calldata erc1155OperatorData) public onlyLlama { 283: function batchApproveOperatorERC1155(ERC1155OperatorData[] calldata erc1155OperatorData) external onlyLlama {
According to the scope information of the project, it is stated that it can be used in rollup chains and popular EVM chains.
We can talk about a systemic risk here because there are some Layer2 special cases. Some conventions in the project are set to Pragma ^0.8.19, allowing the conventions to be compiled with any 0.8.x compiler. The problem with this is that Arbitrum is Compatible with 0.8.20 and newer. Contracts compiled with these versions will result in a non-functional or potentially damaged version that does not behave as expected. The default behavior of the compiler will be to use the latest version which means it will compile with version 0.8.20 which will produce broken code by default.
There is no such onchain governance organization project represented by a non-transferable token.
Successful current security understanding of the project; 1 - First they did the main audit from a reputable auditing organization like SpearBit and resolved all the security concerns in the report 2- They manage the 2nd audit process with an innovative audit such as Code4rena, in which many auditors examine the codes.
What the project should add in the understanding of Security; 1- By distributing the project to testnets, ensuring that the audits are carried out in onchain audit. (This will increase coverage) 2- After the project is published on the mainnet, there should be emergency action plans (not found in the documents)
Automated Findings: https://gist.github.com/itsmetechjay/610f1b31f419156f06898ee10c89402d
2 Medium and 10 Low risks should be examined in detail and recommendations such as _safeMint() should be used rather than _mint() wherever possible
should be considered.
Other Audit Report (SpearBit): https://github.com/code-423n4/2023-06-llama/blob/main/audits/Llama-Spearbit-Audit.pdf
While investigating the potential risks in the project, the past audit reports can give us serious insights, so they should be taken into account and analyzed in detail. When we look at the detail of Spearbit's report, we see that a significant number of security vulnerabilities were detected in the Access Control
category, so in this audit, Access Controls
are also included. should be prioritized;
No | Risk Level | Description | Category |
---|---|---|---|
5.1.1 | Critical Risk | The castApprovalBySig and castDisapprovalBySig functions can revert | Access Control |
5.1.2 | Critical Risk | The castApproval/castDisapproval functions don't check if the role parameter is the approvalRole. | Access Control |
5.2.1 | High Risk | Reducing the quantity of a policyholder results in an increase instead of a decrease in totalQuantity | Logic |
5.3.1 | Medium Risk | LlamaPolicy.revokePolicy cannot be called repeatedly and may result in burned tokens retaining active roles. | Improper Input Validation |
5.3.2 | Medium Risk | Role, permission, strategy, and guard management or config errors may prevent creating/approving/queuing/executing actions. | Improper Input Validation |
5.3.3 | Medium Risk | LlamaPolicy.hasRole doesn't check if a policyholder holds a token | Access Control |
5.3.4 | Medium Risk | Incorrect isActionApproved behavior if new policyholders get added after the createAction in the same block.timestamp | Logic |
5.3.5 | Medium Risk | LlamaCore delegate calls can bring Llama into an unusable state | Access Control |
5.3.6 | Medium Risk | The execution opcode of an action can be changed from call to delegate_call after approval | Access Control |
5.4.1 | Low Risk | LlamaFactory is governed by Llama itself. | Access Control |
5.4.2 | Low Risk | The permissionId doesn't include call or delegate-call for LlamaAccount.execute. | Access Control |
5.4.3 | Low Risk | Nonconforming EIP-712 typehash. | Access Control |
5.4.4 | Low Risk | Various events do not add the role as a parameter. | Access Control |
5.4.5 | Low Risk | LlamaCore doesn't check if minExecutionTime returned by the strategy is in the past. | Logic |
5.4.6 | Low Risk | Address parsing from tokenId to address string does not account for leading 0s. | Data Handling |
5.4.7 | Low Risk | The ALL_HOLDERS_ROLE can be set as a force role by mistake. | Access Control |
5.4.8 | Low Risk | LlamaPolicy.setRolePermission allows setting permissions for non-existing roles. | Access Control |
5.4.9 | Low Risk | The RoleAssigned event in LlamaPolicy emits the currentRoleSupply instead of the quantity. | Data Handling |
5.4.10 | Low Risk | ETH can remain in the contract if msg.value is greater than expected. | Ether Handling |
5.4.11 | Low Risk | Cannot re-authorize an unauthorized strategy config. | Access Control |
5.4.12 | Low Risk | Signed messages may not be canceled. | Access Control |
5.4.13 | Low Risk | LlamaCore name open to squatting or impersonation. | Access Control |
5.4.14 | Low Risk | Expired policyholders are active until they are explicitly revoked. | Access Control |
5.5.1 | Gas Opt. | A few Gas optimizations. | Gas optimization |
5.5.2 | Gas Opt. | Unused code. | Gas optimization |
5.5.3 | Gas Opt. | Duplicate storage reads and external calls. | Gas optimization |
5.5.4 | Gas Opt. | Consider clones-with-immutable-args. | Gas optimization |
5.5.5 | Gas Opt. | The domainSeparator may be cached. | Gas optimization |
5.6.1 | Informational | Prefer on-chain SVGs or IPFS links over server links for contractURI. | Informational |
5.6.2 | Informational | Consider making the delegate-call scripts functions only callable by delegate-call. | Informational |
5.6.3 | Informational | Missing tests for SingleUseScript.sol. | Test |
5.6.4 | Informational | Role not available to Guards. | Informational |
5.6.5 | Informational | Global guards are not supported. | Informational |
5.6.6 | Informational | Consider using _disableInitializers in the constructor. | Informational |
5.6.7 | Informational | Consider renaming initialAccounts to initialAccountNames for clarity. | Informational |
5.6.8 | Informational | Revoking and setting a role edge cases. | Informational |
5.6.9 | Informational | Use built-in string.concat. | Informational |
5.6.10 | Informational | Inconsistencies. | Informational |
5.6.11 | Informational | Policyholders with large quantities may not both create and exercise their large quantity for the same action. | Informational |
5.6.12 | Informational | The roleBalanceCheckpoints can run out of gas. | Informational |
5.6.13 | Informational | GovernanceScript.revokeExpiredRoles should be avoided in favor of calling LlamaPolicy.revokeExpiredRole from EOA. | Informational |
5.6.14 | Informational | The InvalidActionState can be improved. | Informational |
5.6.15 | Informational | _uncheckedIncrement function written in multiple contracts. | Informational |
When the project is evaluated in terms of gas optimization; It is highly gas optimized, if uint256
is used instead of bool
in mapping structures, it will provide even higher gas gain, other gas optimizations are minimal
With the LlamaPolicyMetadata.sol
contract, the use of onchain SVG can consume high gas, since the metadata of NFT is not important for this project, it can increase gas optimization with an offline data usage.
The highest gas expenditure in the project occurs when creating LlamaCore instances, if the architecture here is designed like the singleton architecture in UniswapV4, gas savings will be close to 90%.
https://llama.substack.com/about https://twitter.com/austingreen https://twitter.com/0xrajath
Although the project is not a direct NFT project, NFT projects are colorful projects, different side design architectures add value and interest to the project, such as a bot that tweets when NFT is minted; https://github.com/Cyfrin/foundry-full-course-f23/issues/13 https://github.com/0xAnon0602/Foundery-Course-Twitter-Bot
The project allows us to create a Governance that can be performed with a uniquely non-transferable token
In addition, Script usage is a structure that we can learn differently from this control.
Roles are of type uint8, meaning roles are specified as unsigned integers and the maximum number of roles a Llama instance can have is 255, this part can be increased, the number 255 may be insufficient in the future
The resources and frameworks I use while doing code audit and analysis; https://www.halborn.com/blog/post/delegatecall-vulnerabilities-in-solidity https://book.getfoundry.sh/forge/fuzz-testing Ide: VsCode Ide Extension: Surya , Solidity Metrict Framework: Foundry
A total of 16 hours was spent, 1/3 of the time is reading documentation, 1/3 is code review and the remaining 1/3 is writing reports.
16 hours
#0 - c4-pre-sort
2023-06-19T13:27:08Z
0xSorryNotSorry marked the issue as high quality report
#1 - c4-judge
2023-07-02T15:46:10Z
gzeon-c4 marked the issue as grade-a