Platform: Code4rena
Start Date: 30/06/2023
Pot Size: $100,000 USDC
Total HM: 8
Participants: 22
Period: 14 days
Judge: Trust
Total Solo HM: 6
Id: 253
League: ETH
Rank: 13/22
Findings: 1
Award: $292.81
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Raihan
Also found by: DavidGiladi, LeoS, Rageur, ReyAdmirado, Rolezn, SAAJ, SAQ, SM3_SS, Sathish9098, hunter_w3b, matrix_0wl, naman1778, petrichor
292.8073 USDC - $292.81
no | Issue | Instance |
---|---|---|
[G-01] | Use constants instead of type(uintx).max | 8 |
[G-02] | Empty blocks should be removed or emit something | 2 |
[G-03] | With assembly, .call (bool success) transfer can be done gas-optimized | 2 |
[G-04] | internal functions only called once can be inlined to save gas | 37 |
[G-05] | Multiple accesses of a mapping/array should use a local variable cache | 8 |
[G-06] | Use Assembly To Check For address(0) | 28 |
[G-07] | >= costs less gas than > | 11 |
[G-08] | Duplicated require()/if() checks should be refactored to a modifier or function | 15 |
[G-09] | Use != 0 instead of > 0 for unsigned integer comparison | 8 |
[G-10] | Use hardcode address instead address(this) | 4 |
[G-11] | Access mappings directly rather than using accessor functions | 4 |
[G-12] | Use nested if statements instead of && | 15 |
[G-13] | Can Make The Variable Outside The Loop To Save Gas | 4 |
[G-14] | abi.encode() is less efficient than abi.encodePacked() | 3 |
[G-15] | Use assembly to write address storage values | 3 |
[G-16] | Using calldata instead of memory for read-only arguments in external functions saves gas | 6 |
[G-17] | Uncheck arithmetics operations that can’t underflow/overflow | 20 |
[G-18] | State variables only set in the constructor should be declared immutab | 2 |
[G-19] | Use assembly to hash instead of Solidity | 11 |
it's generally more gas-efficient to use constants instead of type(uintX).max when you need to set the maximum value of an unsigned integer type.
The reason for this is that the type(uintX).max expression involves a computation at runtime, whereas a constant is evaluated at compile-time. This means that using type(uintX).max can result in additional gas costs for each transaction that involves the expression.
By using a constant instead of type(uintX).max, you can avoid these additional gas costs and make your code more efficient.
Here's an example of how you can use a constant instead of type(uintX).max:
87 if (oldArrayLength == type(uint128).max) {
https://github.com/code-423n4/2023-06-lukso/tree/main/contracts/LSP5ReceivedAssets/LSP5Utils.sol#L87
190 if (newArrayLength >= type(uint128).max) {
296 bytes28(type(uint224).max) 378 allowedAddress == address(bytes20(type(uint160).max)) || 395 allowedStandard == bytes4(type(uint32).max) || 414 allowedFunction == bytes4(type(uint32).max) ||
85 if (oldArrayLength == type(uint128).max) { 135 if (oldArrayLength > type(uint128).max) {
When an empty block of code is included in a smart contract, it may not perform any useful operations but still requires gas to be executed. This means that including empty blocks of code in a smart contract can increase the cost of executing the contract, as more gas needs to be paid to include the empty block.
442 function _beforeTokenTransfer( address from, address to, uint256 amount ) internal virtual {}
444 function _beforeTokenTransfer( address from, address to, bytes32 tokenId ) internal virtual {}
When using assembly language, it is possible to call the transfer function of an Ethereum contract in a gas-optimized way by using the .call function with specific input parameters. The .call function takes a number of input parameters, including the address of the contract to call, the amount of Ether to transfer, and a specification of the gas limit for the call. By specifying a lower gas limit than the default, it is possible to reduce the gas cost of the transfer.
186 (bool success, bytes memory returnData) = target.call{value: value}( data );
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725XCore.sol#L186
420 (bool success, bytes memory returnData) = _target.call{ value: msgValue, gas: gasleft() }(payload);
When a function is inlined, its code is inserted directly into the calling function, eliminating the need to create a separate function call and reducing the gas cost of executing the function. This can be particularly beneficial for internal functions that are only called once, as the gas cost of creating and executing a separate function call may be greater than the gas cost of simply inlining the function's code.
131 function _executeBatch( uint256[] memory operationsType, address[] memory targets, uint256[] memory values, bytes[] memory datas ) internal virtual returns (bytes[] memory) {
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725XCore.sol#L131
174 function _executeCall( address target, uint256 value, bytes memory data ) internal virtual returns (bytes memory result) {
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725XCore.sol#L174
203 function _executeStaticCall( address target, bytes memory data ) internal virtual returns (bytes memory result) {
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725XCore.sol#L203
225 function _executeDelegateCall( address target, bytes memory data ) internal virtual returns (bytes memory result) {
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725XCore.sol#L225
247 function _deployCreate( uint256 value, bytes memory creationCode ) internal virtual returns (bytes memory newContract) {
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725XCore.sol#L247
288 function _deployCreate2( uint256 value, bytes memory creationCode ) internal virtual returns (bytes memory newContract) {
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725XCore.sol#L288
796 function _fallbackLSP17Extendable() internal virtual override {
851 function _getExtension( bytes4 functionSelector ) internal view virtual override returns (address) {
152 function _whenReceiving( bytes32 typeId, address notifier, bytes32 notifierMapKey, bytes4 interfaceID ) internal virtual returns (bytes memory) {
201 function _whenSending( bytes32 typeId, address notifier, bytes32 notifierMapKey, uint128 arrayIndex ) internal virtual returns (bytes memory) {
27 function _initialize( string memory name_, string memory symbol_, address newOwner_ ) internal virtual onlyInitializing {
153 function _verifyCanDeployContract( address controller, bytes32 permissions, bool isFundingContract ) internal view virtual {
170 function _verifyCanStaticCall( address controlledContract, address controller, bytes32 permissions, bytes calldata payload ) internal view virtual {
188 function _verifyCanCall( address controlledContract, address controller, bytes32 permissions, bytes calldata payload ) internal view virtual {
317 function _extractCallType( uint256 operationType, uint256 value, bool isEmptyCall ) internal pure returns (bytes4 requiredCallTypes) {
339 function _extractExecuteParameters( bytes calldata executeCalldata ) internal pure returns (uint256, address, uint256, bytes4, bool) {
366 function _isAllowedAddress( bytes memory allowedCall, address to ) internal pure returns (bool) {
382 function _isAllowedStandard( bytes memory allowedCall, address to ) internal view returns (bool) {
399 function _isAllowedFunction( bytes memory allowedCall, bytes4 requiredFunction ) internal pure returns (bool) {
418 function _isAllowedCallType( bytes memory allowedCall, bytes4 requiredCallTypes ) internal pure returns (bool) {
441 function _isValidNonce( address from, uint256 idx ) internal view virtual returns (bool) {
334 function _getPermissionToSetPermissionsArray( address controlledContract, bytes32 inputDataKey, bytes memory inputDataValue, bool hasBothAddControllerAndEditPermissions ) internal view virtual returns (bytes32) {
385 function _getPermissionToSetControllerPermissions( address controlledContract, bytes32 inputPermissionDataKey ) internal view virtual returns (bytes32) {
406 function _getPermissionToSetAllowedCalls( address controlledContract, bytes32 dataKey, bytes memory dataValue, bool hasBothAddControllerAndEditPermissions ) internal view virtual returns (bytes32) {
438 function _getPermissionToSetAllowedERC725YDataKeys( address controlledContract, bytes32 dataKey, bytes memory dataValue, bool hasBothAddControllerAndEditPermissions ) internal view returns (bytes32) {
474 function _getPermissionToSetLSP1Delegate( address controlledContract, bytes32 lsp1DelegateDataKey ) internal view virtual returns (bytes32) {
490 function _getPermissionToSetLSP17Extension( address controlledContract, bytes32 lsp17ExtensionDataKey ) internal view virtual returns (bytes32) {
507 function _verifyAllowedERC725YSingleKey( address controllerAddress, bytes32 inputDataKey, bytes memory allowedERC725YDataKeysCompacted ) internal pure virtual {
622 function _verifyAllowedERC725YDataKeys( address controllerAddress, bytes32[] memory inputDataKeys, bytes memory allowedERC725YDataKeysCompacted, bool[] memory validatedInputKeysList, uint256 allowedDataKeysFound ) internal pure virtual {
53 function _mint( address to, uint256 amount, bool allowNonLSP1Recipient, bytes memory data ) internal virtual override {
107 function _updateOperator( address tokenOwner, address operator, uint256 amount ) internal virtual override {
116 function _transfer( address from, address to, uint256 amount, bool allowNonLSP1Recipient, bytes memory data ) internal virtual override {
127 function _mint( address to, uint256 amount, bool allowNonLSP1Recipient, bytes memory data ) internal virtual override {
137 function _burn( address from, uint256 amount, bytes memory data ) internal virtual override {
146 function _setData( bytes32 key, bytes memory value ) internal virtual override(LSP4DigitalAssetMetadata, ERC725YCore) {
399 function _transfer( address from, address to, uint256 amount, bool allowNonLSP1Recipient, bytes memory data ) internal virtual {
28 function _initialize( string memory name_, string memory symbol_, address newOwner_, bool isNonDivisible_ ) internal virtual onlyInitializing {
One way to optimize the gas cost of accessing mappings or arrays multiple times is to use a local variable cache. This involves storing the value of the mapping or array in a local variable and then accessing the local variable multiple times instead of accessing the mapping or array directly.
38 _indexToken[index] = tokenId;
44 bytes32 lastTokenId = _indexToken[lastIndex];
45 _indexToken[index] = lastTokenId;
48 delete _indexToken[lastIndex];
39 _tokenIndex[tokenId] = index;
42 uint256 index = _tokenIndex[tokenId];
46 _tokenIndex[lastTokenId] = index;
49 delete _tokenIndex[tokenId];
One way to check for the zero address using assembly language is to use the iszero opcode, which returns true if the input value is zero and false otherwise. We can use this opcode to check whether an address is equal to the zero address by converting the address to a 256-bit integer and then checking whether the integer value is zero.
28 newOwner != address(0),
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725.sol#L28
28 newOwner != address(0),
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725InitAbstract
22 newOwner != address(0),
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725X.sol#L22
89 if (target != address(0)) 96 if (target != address(0)) 296 if (contractAddress == address(0)) {
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725XCore.sol#L89
21 newOwner != address(0),
23 newOwner != address(0),
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725Y.sol#L23
21 newOwner != address(0),
801 if (msg.sig == bytes4(0) && extension == address(0)) return; 804 if (extension == address(0))
19 if (target_ == address(0)) revert InvalidLSP6Target();
21 if (target_ == address(0)) revert InvalidLSP6Target();
268 if (operator == address(0)) { 300 if (to == address(0)) { 343 if (from == address(0)) { 406 if (from == address(0) || to == address(0)) {
36 if (from == address(0)) { 40 } else if (to == address(0)) {
38 if (from == address(0)) { 42 } else if (to == address(0)) {
84 if (tokenOwner == address(0)) { 115 if (operator == address(0)) { 139 if (operator == address(0)) { 319 if (to == address(0)) { 410 if (to == address(0)) {
50 if (erc165Extension == address(0)) return false; 91 if (extension == address(0))
51 newOwner != address(0),
In Solidity, the comparison operators > and >= are used to compare two values and return a boolean value indicating whether the comparison is true or false. However, it's important to note that using >= can often be more gas-efficient than using >.
The reason for this is that the EVM (Ethereum Virtual Machine) uses a 256-bit word size, which means that operations involving values smaller than 256 bits still require a full 256 bits of data to be loaded into memory. This means that when we use > to compare two values, we may end up loading more data into memory than necessary, resulting in higher gas costs.
182 if (result.length > 0) { 741 if (_owner.code.length > 0) {
165 if (notifier.code.length > 0) {
284 if (ii + 34 > allowedCalls.length) {
559 if (length > 32) 679 if (length > 32)
137 if (elementLength == 0 || elementLength > 32) return false;
https://github.com/code-423n4/2023-06-lukso/tree/main/contracts/LSP6KeyManager/LSP6Utils.sol#L137
136 if (amount > operatorAmount) { 348 if (amount > balance) { 357 if (amount > authorizedAmount) { 491 if (to.code.length > 0) {
To optimize the gas cost of a contract and reduce redundant code, it's often recommended to refactor duplicated require() or if() checks into a modifier or function. This can help to reduce the size of the contract and make it easier to read and maintain.
89 if (target != address(0)) 96 if (target != address(0)) 179 if (address(this).balance < value) { 251 if (address(this).balance < value) {
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725XCore.sol#L89
66 if (msg.value != 0) revert ERC725Y_MsgValueDisallowed(); 78 if (msg.value != 0) revert ERC725Y_MsgValueDisallowed();
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725YCore.sol#L66
118 if (msg.value != 0) { 152 if (msg.value != 0) { 228 if (msg.value != 0) { 286 if (msg.value != 0) { 343 if (msg.value != 0) { 384 if (msg.value != 0) { 464 if (msg.value != 0) { 235 if (msg.sender == _owner) { 293 if (msg.sender == _owner) {
In Solidity, unsigned integer values can be compared using either the > or != operators. However, it's generally more gas-efficient to use the != 0 operator instead of the > 0 operator, especially when dealing with large integer values.
The reason for this is that the EVM (Ethereum Virtual Machine) uses a 256-bit word size, which means that operations involving values smaller than 256 bits still require a full 256 bits of data to be loaded into memory. This means that when we use the > 0 operator to compare an unsigned integer value, we may end up loading more data into memory than necessary, resulting in higher gas costs.
182 if (result.length > 0) {
741 if (_owner.code.length > 0) {
165 if (notifier.code.length > 0) {
491 if (to.code.length > 0) {
313 if (to.code.length > 0) {
313 if (to.code.length > 0) {
493 if (to.code.length > 0) {
86 if (returnedData.length > 0) {
The reason for this is that address(this) requires the EVM to perform a CALL operation to obtain the address of the current contract. This operation can be relatively expensive in terms of gas costs, especially if it's performed repeatedly within the same contract.
179 if (address(this).balance < value) {
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725XCore.sol#L179
180 revert ERC725X_InsufficientBalance(address(this).balance, value);
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725XCore.sol#L180
251 if (address(this).balance < value) {
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725XCore.sol#L251
252 revert ERC725X_InsufficientBalance(address(this).balance, value);
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725XCore.sol#L252
In Solidity, it's common to use accessor functions to retrieve values from mappings. However, in some cases, it can be more gas-efficient to access the mapping directly, especially if the mapping is not too large. The reason for this is that accessor functions typically involve additional function call overhead, which can add to the gas cost of the contract. If we access the mapping directly, we can avoid this overhead and reduce the gas cost of the contract.
99 return _store[dataKey];
https://github.com/ERC725Alliance/ERC725/blob/v5.1.0/implementations/contracts/ERC725YCore.sol#L99
28 return _indexToken[index];
30 return _indexToken[index];
73 return _tokenOwnerBalances[tokenOwner];
The reason for this is that && requires the EVM to evaluate both conditions and perform a logical operation to combine the results. This can be relatively expensive in terms of gas costs, especially if the conditions involve complex expressions or function calls.
801 if (msg.sig == bytes4(0) && extension == address(0)) return;
227 if (dataKeys.length == 0 && dataValues.length == 0) 245 if (dataKeys.length == 0 && dataValues.length == 0)
78 if (encodedArrayLength.length != 0 && encodedArrayLength.length != 16) {
https://github.com/code-423n4/2023-06-lukso/tree/main/contracts/LSP5ReceivedAssets/LSP5Utils.sol#L78
165 if (isFundingContract && !hasSuperTransferValue) { 214 if (isTransferringValue && !hasSuperTransferValue) { 224 if (!hasSuperCall && !isCallDataPresent && !isTransferringValue) { 228 if (isCallDataPresent && !hasSuperCall) { 236 if (hasSuperTransferValue && !isCallDataPresent && isTransferringValue) 240 if (hasSuperCall && hasSuperTransferValue) return;
338 if (!isReentrantCall && !isSetData) {
404 if (!isReentrantCall && !isSetData) {
362 if (inputDataValue.length != 0 && inputDataValue.length != 20) {
76 if (encodedArrayLength.length != 0 && encodedArrayLength.length != 16) {
The reason for this is that each time a variable is declared inside a loop, it consumes gas to allocate memory for the variable. If the loop is executed multiple times, this can result in redundant gas costs for memory allocation.
176 (bool success, bytes memory result) = address(this).delegatecall(
262 bytes32 key = data.toBytes32(pointer);
293 bytes32 key = data.toBytes32(pointer);
275 address operator = operatorsForTokenId.at(0);
The reason for this is that abi.encode() adds additional metadata to the encoded data, including the function signature and the length of the encoded data. This metadata can be useful in some cases, but it also adds to the gas cost of the encoding operation.
253 LSP20CallVerification._verifyCallResult(_owner, abi.encode(result));
319 abi.encode(results)
528 returnedValues = abi.encode(
One use case where assembly can be more gas-efficient is when writing to storage, especially if we're writing to multiple storage slots in a single operation. Using Solidity code to write to storage can result in redundant gas costs for memory allocation, whereas assembly allows us to write to multiple storage slots in a single operation, which can reduce the gas cost of the contract.
129 _pendingOwner = newOwner;
71 _owner = newOwner;
22 _target = target_;
When a function is called externally in Solidity, the arguments are passed in the calldata, which is a special area of memory that is used to store the function arguments and any other data that is required for the function call. By using calldata instead of memory for read-only arguments, we can avoid the need for memory allocation and copy operations, which can reduce the gas cost of the contract.
93 function executeBatch( uint256[] memory operationsType, address[] memory targets, uint256[] memory values, bytes[] memory datas ) external payable returns (bytes[] memory);
35 function getDataBatch( bytes32[] memory dataKeys ) external view returns (bytes[] memory dataValues); 52 function setData(bytes32 dataKey, bytes memory dataValue) external payable; 67 function setDataBatch( bytes32[] memory dataKeys, bytes[] memory dataValues ) external payable;
19 function lsp20VerifyCall( address caller, uint256 value, bytes memory receivedCalldata ) external returns (bytes4 magicValue); 31 function lsp20VerifyCallResult( bytes32 callHash, bytes memory result ) external returns (bytes4);
In Solidity, performing arithmetic operations that cannot underflow or overflow can be more gas-efficient if we can guarantee that the operations will not result in an overflow or underflow condition. This is because Solidity automatically checks for overflow and underflow conditions during arithmetic operations, which can result in additional gas costs.
By using unchecked arithmetic operations, we can avoid the need for these checks and reduce the gas cost of the contract. However, it's important to note that unchecked arithmetic operations can be dangerous if we can't guarantee that the operations will not result in an overflow or underflow condition. If an overflow or underflow condition occurs, it can result in unexpected behavior and potentially compromise the security of the contract.
269 pointer += 32; 299 pointer += 32; 344 pointer += elementLength + 2;
137 uint128 newArrayLength = oldArrayLength - 1;
108 pointer += elementLength + 2; 138 pointer += elementLength + 2; 174 result += uint256(permissions[i]);
https://github.com/code-423n4/2023-06-lukso/tree/main/contracts/LSP6KeyManager/LSP6Utils.sol#L137
145 _updateOperator(from, operator, operatorAmount - amount); 365 _operatorAuthorizedAmount[from][operator] -= amount; 371 _existingTokens -= amount; 373 _tokenOwnerBalances[from] -= amount; 419 _tokenOwnerBalances[from] -= amount; 420 _tokenOwnerBalances[to] += amount;
102 bytes memory uriBytes = BytesLib.slice( data, offset, data.length - offset ); 138 return operatorsForTokenId[operatorListLength - 1];
102 bytes memory uriBytes = BytesLib.slice( data, offset, data.length - offset ); 138 return operatorsForTokenId[operatorListLength - 1];
41 uint256 lastIndex = totalSupply() - 1;
43 uint256 lastIndex = totalSupply() - 1;
140 uint128 newArrayLength = oldArrayLength - 1;
When a state variable is modified in Solidity, it requires a storage write operation, which can consume a significant amount of gas. However, immutable variables are stored differently in storage, and their value is directly embedded in the bytecode of the contract, which means that they do not require storage write operations when they are accessed.
20 _target = target_;
45 _isNonDivisible = isNonDivisible_;
When we perform cryptographic hashing operations in Solidity, we typically use the built-in Solidity hash functions such as keccak256(). However, these functions can be relatively expensive in terms of gas cost, especially if we need to perform multiple hashing operations.
25 return keccak256(bytes(keyName)); 43 return keccak256(dataKey); 75 bytes32 firstWordHash = keccak256(bytes(firstWord)); 76 bytes32 lastWordHash = keccak256(bytes(lastWord)); 98 bytes32 firstWordHash = keccak256(bytes(firstWord)); 141 bytes32 firstWordHash = keccak256(bytes(firstWord)); 142 bytes32 secondWordHash = keccak256(bytes(secondWord)); 204 bytes32 hashFunctionDigest = keccak256(bytes(hashFunction)); 205 bytes32 jsonDigest = keccak256(bytes(json)); 221 bytes32 hashFunctionDigest = keccak256(bytes(hashFunction)); 222 bytes32 jsonDigest = keccak256(bytes(assetBytes));
#0 - c4-judge
2023-08-02T09:50:28Z
trust1995 marked the issue as grade-a
#1 - trust1995
2023-08-02T09:53:33Z
Suspiciously similar to #89 , flagged to staff .