Platform: Code4rena
Start Date: 13/01/2023
Pot Size: $100,500 USDC
Total HM: 1
Participants: 23
Period: 10 days
Judge: hickuphh3
Total Solo HM: 1
Id: 201
League: ETH
Rank: 8/23
Findings: 2
Award: $310.43
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: horsefacts
Also found by: 0xSmartContract, ABA, Chom, IllIllI, Josiah, RaymondFam, Rickard, Rolezn, brgltd, btk, chaduke, charlesjhongc, csanuragjain, delfin454000, nadin, oyc_109
140.6728 USDC - $140.67
Issue | Contexts | |
---|---|---|
LOW‑1 | Low Level Calls With Solidity Version 0.8.14 Can Result In Optimiser Bug | 3 |
LOW‑2 | Missing Checks for Address(0x0) | 2 |
LOW‑3 | Remove unused code | 1 |
LOW‑4 | Use safeTransferOwnership instead of transferOwnership function | 1 |
Total: 7 contexts over 3 issues
Issue | Contexts | |
---|---|---|
NC‑1 | Compliance with Solidity Style rules in Constant expressions | All in-scope contracts |
NC‑2 | Function creates dirty bits | 8 |
NC‑3 | Function writing that does not comply with the Solidity Style Guide | All in-scope contracts |
NC‑4 | Use delete to Clear Variables | 1 |
NC‑5 | NatSpec return parameters should be included in contracts | All in-scope contracts |
NC‑6 | No need to initialize uints to zero | 3 |
NC‑7 | Implementation contract may not be initialized | 15 |
NC‑8 | NatSpec comments should be increased in contracts | All in-scope contracts |
NC‑9 | Non-usage of specific imports | 28 |
NC‑10 | Use a more recent version of Solidity | 32 |
NC‑11 | Omissions in Events | 1 |
NC‑12 | Adding A Return Statement When The Function Defines A Named Return Variable, Is Redundant | 2 |
NC‑13 | Use bytes.concat() | 3 |
Total: 309 contexts over 13 issues
The project contracts in scope are using low level calls with solidity version before 0.8.14 which can result in optimizer bug. https://medium.com/certora/overly-optimistic-optimizer-certora-bug-disclosure-2101e3f7994d
Simliar findings in Code4rena contests for reference: https://code4rena.com/reports/2022-06-illuminate/#5-low-level-calls-with-solidity-version-0814-can-result-in-optimiser-bug
POC can be found in the above medium reference url.
Functions that execute low level calls in contracts with solidity version under 0.8.14
29: assembly { mPtr := mload(0x40) mstore(0x40, add(mPtr, size)) }
3081: assembly { mstore(mPtr, value) }
3074: assembly { mstore(mPtr, value) }
Consider upgrading to at least solidity v0.8.15.
Lack of zero-address validation on address parameters may lead to transaction reverts, waste gas, require resubmission of transactions and may even force contract redeployments in certain cases within the protocol.
126: function updateChannel: address channel
Consider adding explicit zero-address validation on input parameters of address type.
This code is not used in the main project contract files, remove it or add event-emit Code that is not in use, suggests that they should not be present and could potentially contain insecure functionalities.
function writeInt(MemoryPointer mPtr, int256 value) internal pure { assembly { mstore(mPtr, value) } }
safeTransferOwnership
instead of transferOwnership
functionUse safeTransferOwnership
which is safer. Use it as it is more secure due to 2-stage ownership transfer.
202: function transferOwnership: function transferOwnership(
Use <a href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable2Step.sol">Ownable2Step.sol</a>
function acceptOwnership() external { address sender = _msgSender(); require(pendingOwner() == sender, "Ownable2Step: caller is not the new owner"); _transferOwnership(sender); } }
Here are following some examples, but this issue persists thorugh all in-scope contracts.
5: uint256 constant ChannelClosed_error_signature = (
8: uint256 constant ChannelClosed_error_ptr = 0x00;
9: uint256 constant ChannelClosed_channel_ptr = 0x4;
10: uint256 constant ChannelClosed_error_length = 0x24;
16: uint256 constant ChannelKey_channel_ptr = 0x00;
17: uint256 constant ChannelKey_slot_ptr = 0x20;
18: uint256 constant ChannelKey_length = 0x40;
19: CalldataPointer constant CalldataStart = CalldataPointer.wrap(0x04);
20: MemoryPointer constant FreeMemoryPPtr = MemoryPointer.wrap(0x40);
21: uint256 constant IdentityPrecompileAddress = 4;
22: uint256 constant OffsetOrLengthMask = 0xffffffff;
41: uint256 constant NameLengthPtr = 77;
42: uint256 constant NameWithLength = 0x0d436F6E73696465726174696F6E;
44: uint256 constant information_version_offset = 0;
This explanation should be added in the NatSpec comments of this function that sends ether with call;
Note that this code probably isn’t secure or a good use case for assembly because a lot of memory management and security checks are bypassed. Use with caution! Some functions in this contract knowingly create dirty bits at the destination of the free memory pointer.
241: function _transferNativeTokens( address payable to, uint256 amount ) internal { _assertNonZeroAmount(amount); bool success; assembly { success := call(gas(), to, amount, 0, 0, 0, 0) } if (!success) { _revertWithReasonIfOneIsReturned(); assembly { mstore(0, EtherTransferGenericFailure_error_selector) mstore(EtherTransferGenericFailure_error_account_ptr, to) mstore(EtherTransferGenericFailure_error_amount_ptr, amount) revert( Error_selector_offset, EtherTransferGenericFailure_error_length ) } } }
538: function _callConduitUsingOffsets( bytes32 conduitKey, uint256 callDataOffset, uint256 callDataSize ) internal { address conduit = _deriveConduit(conduitKey); bool success; bytes4 result; assembly { mstore(0, 0) success := call( gas(), conduit, 0, callDataOffset, callDataSize, 0, OneWord ) result := mload(0) } if (!success) { _revertWithReasonIfOneIsReturned(); _revertInvalidCallToConduit(conduit); } if (result != ConduitInterface.execute.selector) { _revertInvalidConduit(conduitKey, conduit); } }
497: function _getGeneratedOrder( OrderParameters memory orderParameters, bytes memory context, bool revertOnInvalid ) internal returns (bytes32 orderHash, uint256 numerator, uint256 denominator) { if ( orderParameters.consideration.length != orderParameters.totalOriginalConsiderationItems ) { _revertConsiderationLengthNotEqualToTotalOriginal(); } { address offerer = orderParameters.offerer; bool success; (MemoryPointer cdPtr, uint256 size) = _encodeGenerateOrder( orderParameters, context ); assembly { success := call(gas(), offerer, 0, cdPtr, size, 0, 0) } { uint256 contractNonce; unchecked { contractNonce = _contractNonces[offerer]++; } assembly { orderHash := xor( contractNonce, shl(ContractOrder_orderHash_offerer_shift, offerer) ) } } if (!success) { return _revertOrReturnEmpty(revertOnInvalid, orderHash); } } ( uint256 errorBuffer, OfferItem[] memory offer, ConsiderationItem[] memory consideration ) = _convertGetGeneratedOrderResult(_decodeGenerateOrderReturndata)(); if (errorBuffer != 0) { return _revertOrReturnEmpty(revertOnInvalid, orderHash); } { uint256 originalOfferLength = orderParameters.offer.length; uint256 newOfferLength = offer.length; if (originalOfferLength > newOfferLength) { return _revertOrReturnEmpty(revertOnInvalid, orderHash); } for (uint256 i = 0; i < originalOfferLength; ) { MemoryPointer mPtrOriginal = orderParameters .offer[i] .toMemoryPointer(); MemoryPointer mPtrNew = offer[i].toMemoryPointer(); errorBuffer |= _cast( mPtrOriginal .offset(Common_amount_offset) .readUint256() > mPtrNew.offset(Common_amount_offset).readUint256() ) | _compareItems(mPtrOriginal, mPtrNew); unchecked { ++i; } } orderParameters.offer = offer; } { ConsiderationItem[] memory originalConsiderationArray = ( orderParameters.consideration ); uint256 newConsiderationLength = consideration.length; if (newConsiderationLength > originalConsiderationArray.length) { return _revertOrReturnEmpty(revertOnInvalid, orderHash); } for (uint256 i = 0; i < newConsiderationLength; ) { MemoryPointer mPtrOriginal = originalConsiderationArray[i] .toMemoryPointer(); MemoryPointer mPtrNew = consideration[i].toMemoryPointer(); errorBuffer |= _cast( mPtrNew.offset(Common_amount_offset).readUint256() > mPtrOriginal .offset(Common_amount_offset) .readUint256() ) | _compareItems(mPtrOriginal, mPtrNew) | _checkRecipients( mPtrOriginal .offset(ConsiderItem_recipient_offset) .readAddress(), mPtrNew .offset(ConsiderItem_recipient_offset) .readAddress() ); unchecked { ++i; } } orderParameters.consideration = consideration; } if (errorBuffer != 0) { return _revertOrReturnEmpty(revertOnInvalid, orderHash); } return (orderHash, 1, 1); }
59: function _performERC20Transfer( address token, address from, address to, uint256 amount ) internal { assembly { let memPointer := mload(FreeMemoryPointerSlot) mstore(ERC20_transferFrom_sig_ptr, ERC20_transferFrom_signature) mstore(ERC20_transferFrom_from_ptr, from) mstore(ERC20_transferFrom_to_ptr, to) mstore(ERC20_transferFrom_amount_ptr, amount) let callStatus := call( gas(), token, 0, ERC20_transferFrom_sig_ptr, ERC20_transferFrom_length, 0, OneWord ) let success := and( or( and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize()) ), callStatus ) if iszero(and(success, iszero(iszero(returndatasize())))) { if iszero(and(iszero(iszero(extcodesize(token))), success)) { if iszero(success) { if iszero(callStatus) { if returndatasize() { let returnDataWords := shr( OneWordShift, add(returndatasize(), AlmostOneWord) ) let msizeWords := shr(OneWordShift, memPointer) let cost := mul(CostPerWord, returnDataWords) if gt(returnDataWords, msizeWords) { cost := add( cost, add( mul( sub( returnDataWords, msizeWords ), CostPerWord ), shr( MemoryExpansionCoefficientShift, sub( mul( returnDataWords, returnDataWords ), mul(msizeWords, msizeWords) ) ) ) ) } if lt(add(cost, ExtraGasBuffer), gas()) { returndatacopy(0, 0, returndatasize()) revert(0, returndatasize()) } } mstore( 0, TokenTransferGenericFailure_error_selector ) mstore( TokenTransferGenericFailure_error_token_ptr, token ) mstore( TokenTransferGenericFailure_error_from_ptr, from ) mstore(TokenTransferGenericFailure_error_to_ptr, to) mstore( TokenTransferGenericFailure_err_identifier_ptr, 0 ) mstore( TokenTransferGenericFailure_error_amount_ptr, amount ) revert( Generic_error_selector_offset, TokenTransferGenericFailure_error_length ) } mstore( 0, BadReturnValueFromERC20OnTransfer_error_selector ) mstore( BadReturnValueFromERC20OnTransfer_error_token_ptr, token ) mstore( BadReturnValueFromERC20OnTransfer_error_from_ptr, from ) mstore( BadReturnValueFromERC20OnTransfer_error_to_ptr, to ) mstore( BadReturnValueFromERC20OnTransfer_error_amount_ptr, amount ) revert( Generic_error_selector_offset, BadReturnValueFromERC20OnTransfer_error_length ) } mstore(0, NoContract_error_selector) mstore(NoContract_error_account_ptr, token) revert( Generic_error_selector_offset, NoContract_error_length ) } } mstore(FreeMemoryPointerSlot, memPointer) mstore(ZeroSlot, 0) } }
296: function _performERC721Transfer( address token, address from, address to, uint256 identifier ) internal { assembly { if iszero(extcodesize(token)) { mstore(0, NoContract_error_selector) mstore(NoContract_error_account_ptr, token) revert(Generic_error_selector_offset, NoContract_error_length) } let memPointer := mload(FreeMemoryPointerSlot) mstore(ERC721_transferFrom_sig_ptr, ERC721_transferFrom_signature) mstore(ERC721_transferFrom_from_ptr, from) mstore(ERC721_transferFrom_to_ptr, to) mstore(ERC721_transferFrom_id_ptr, identifier) let success := call( gas(), token, 0, ERC721_transferFrom_sig_ptr, ERC721_transferFrom_length, 0, 0 ) if iszero(success) { if returndatasize() { let returnDataWords := shr( OneWordShift, add(returndatasize(), AlmostOneWord) ) let msizeWords := shr(OneWordShift, memPointer) let cost := mul(CostPerWord, returnDataWords) if gt(returnDataWords, msizeWords) { cost := add( cost, add( mul( sub(returnDataWords, msizeWords), CostPerWord ), shr( MemoryExpansionCoefficientShift, sub( mul(returnDataWords, returnDataWords), mul(msizeWords, msizeWords) ) ) ) ) } if lt(add(cost, ExtraGasBuffer), gas()) { returndatacopy(0, 0, returndatasize()) revert(0, returndatasize()) } } mstore(0, TokenTransferGenericFailure_error_selector) mstore(TokenTransferGenericFailure_error_token_ptr, token) mstore(TokenTransferGenericFailure_error_from_ptr, from) mstore(TokenTransferGenericFailure_error_to_ptr, to) mstore( TokenTransferGenericFailure_error_identifier_ptr, identifier ) mstore(TokenTransferGenericFailure_error_amount_ptr, 1) revert( Generic_error_selector_offset, TokenTransferGenericFailure_error_length ) } mstore(FreeMemoryPointerSlot, memPointer) mstore(ZeroSlot, 0) } }
448: function _performERC1155Transfer( address token, address from, address to, uint256 identifier, uint256 amount ) internal { assembly { if iszero(extcodesize(token)) { mstore(0, NoContract_error_selector) mstore(NoContract_error_account_ptr, token) revert(Generic_error_selector_offset, NoContract_error_length) } let memPointer := mload(FreeMemoryPointerSlot) let slot0x80 := mload(Slot0x80) let slot0xA0 := mload(Slot0xA0) let slot0xC0 := mload(Slot0xC0) mstore( ERC1155_safeTransferFrom_sig_ptr, ERC1155_safeTransferFrom_signature ) mstore(ERC1155_safeTransferFrom_from_ptr, from) mstore(ERC1155_safeTransferFrom_to_ptr, to) mstore(ERC1155_safeTransferFrom_id_ptr, identifier) mstore(ERC1155_safeTransferFrom_amount_ptr, amount) mstore( ERC1155_safeTransferFrom_data_offset_ptr, ERC1155_safeTransferFrom_data_length_offset ) mstore(ERC1155_safeTransferFrom_data_length_ptr, 0) let success := call( gas(), token, 0, ERC1155_safeTransferFrom_sig_ptr, ERC1155_safeTransferFrom_length, 0, 0 ) if iszero(success) { if returndatasize() { let returnDataWords := shr( OneWordShift, add(returndatasize(), AlmostOneWord) ) let msizeWords := shr(OneWordShift, memPointer) let cost := mul(CostPerWord, returnDataWords) if gt(returnDataWords, msizeWords) { cost := add( cost, add( mul( sub(returnDataWords, msizeWords), CostPerWord ), shr( MemoryExpansionCoefficientShift, sub( mul(returnDataWords, returnDataWords), mul(msizeWords, msizeWords) ) ) ) ) } if lt(add(cost, ExtraGasBuffer), gas()) { returndatacopy(0, 0, returndatasize()) revert(0, returndatasize()) } } mstore(0, TokenTransferGenericFailure_error_selector) mstore(TokenTransferGenericFailure_error_token_ptr, token) mstore(TokenTransferGenericFailure_error_from_ptr, from) mstore(TokenTransferGenericFailure_error_to_ptr, to) mstore( TokenTransferGenericFailure_error_identifier_ptr, identifier ) mstore(TokenTransferGenericFailure_error_amount_ptr, amount) revert( Generic_error_selector_offset, TokenTransferGenericFailure_error_length ) } mstore(Slot0x80, slot0x80) mstore(Slot0xA0, slot0xA0) mstore(Slot0xC0, slot0xC0) mstore(FreeMemoryPointerSlot, memPointer) mstore(ZeroSlot, 0) } }
723: function _performERC1155BatchTransfers( ConduitBatch1155Transfer[] calldata batchTransfers ) internal { assembly { let len := batchTransfers.length let nextElementHeadPtr := batchTransfers.offset let arrayHeadPtr := nextElementHeadPtr mstore( ConduitBatch1155Transfer_from_offset, ERC1155_safeBatchTransferFrom_signature ) for { let i := 0 } lt(i, len) { i := add(i, 1) } { let elementPtr := add( arrayHeadPtr, calldataload(nextElementHeadPtr) ) let token := calldataload(elementPtr) if iszero(extcodesize(token)) { mstore(0, NoContract_error_selector) mstore(NoContract_error_account_ptr, token) revert( Generic_error_selector_offset, NoContract_error_length ) } let idsLength := calldataload( add(elementPtr, ConduitBatch1155Transfer_ids_length_offset) ) let expectedAmountsOffset := add( ConduitBatch1155Transfer_amounts_length_baseOffset, shl(OneWordShift, idsLength) ) let invalidEncoding := iszero( and( eq( idsLength, calldataload(add(elementPtr, expectedAmountsOffset)) ), and( eq( calldataload( add( elementPtr, ConduitBatch1155Transfer_ids_head_offset ) ), ConduitBatch1155Transfer_ids_length_offset ), eq( calldataload( add( elementPtr, ConduitBatchTransfer_amounts_head_offset ) ), expectedAmountsOffset ) ) ) ) if invalidEncoding { mstore( Invalid1155BatchTransferEncoding_ptr, Invalid1155BatchTransferEncoding_selector ) revert( Invalid1155BatchTransferEncoding_ptr, Invalid1155BatchTransferEncoding_length ) } nextElementHeadPtr := add(nextElementHeadPtr, OneWord) calldatacopy( BatchTransfer1155Params_ptr, add(elementPtr, ConduitBatch1155Transfer_from_offset), ConduitBatch1155Transfer_usable_head_size ) let idsAndAmountsSize := add( TwoWords, shl(TwoWordsShift, idsLength) ) mstore( BatchTransfer1155Params_data_head_ptr, add( BatchTransfer1155Params_ids_length_offset, idsAndAmountsSize ) ) mstore( add( BatchTransfer1155Params_data_length_basePtr, idsAndAmountsSize ), 0 ) let transferDataSize := add( BatchTransfer1155Params_calldata_baseSize, idsAndAmountsSize ) calldatacopy( BatchTransfer1155Params_ids_length_ptr, add(elementPtr, ConduitBatch1155Transfer_ids_length_offset), idsAndAmountsSize ) let success := call( gas(), token, 0, ConduitBatch1155Transfer_from_offset, transferDataSize, 0, 0 ) if iszero(success) { if returndatasize() { let returnDataWords := shr( OneWordShift, add(returndatasize(), AlmostOneWord) ) let msizeWords := shr(OneWordShift, transferDataSize) let cost := mul(CostPerWord, returnDataWords) if gt(returnDataWords, msizeWords) { cost := add( cost, add( mul( sub(returnDataWords, msizeWords), CostPerWord ), shr( MemoryExpansionCoefficientShift, sub( mul( returnDataWords, returnDataWords ), mul(msizeWords, msizeWords) ) ) ) ) } if lt(add(cost, ExtraGasBuffer), gas()) { returndatacopy(0, 0, returndatasize()) revert(0, returndatasize()) } } mstore( 0, ERC1155BatchTransferGenericFailure_error_signature ) mstore(ERC1155BatchTransferGenericFailure_token_ptr, token) mstore( BatchTransfer1155Params_ids_head_ptr, ERC1155BatchTransferGenericFailure_ids_offset ) mstore( BatchTransfer1155Params_amounts_head_ptr, add( OneWord, mload(BatchTransfer1155Params_amounts_head_ptr) ) ) revert(0, transferDataSize) } } mstore(FreeMemoryPointerSlot, DefaultFreeMemoryPointer) } } }
182: function _callAndCheckStatus( address target, bytes32 orderHash, MemoryPointer callData, uint256 size, uint256 errorSelector ) internal { bool success; bool magicMatch; assembly { let magic := and(mload(callData), MaskOverFirstFourBytes) mstore(0, 0) success := call(gas(), target, 0, callData, size, 0, OneWord) magicMatch := eq(magic, mload(0)) } if (!success) { _revertWithReasonIfOneIsReturned(); assembly { mstore(0, errorSelector) mstore(InvalidRestrictedOrder_error_orderHash_ptr, orderHash) revert( Error_selector_offset, InvalidRestrictedOrder_error_length ) } } if (!magicMatch) { assembly { mstore(0, errorSelector) mstore(InvalidRestrictedOrder_error_orderHash_ptr, orderHash) revert( Error_selector_offset, InvalidRestrictedOrder_error_length ) } } } }
Add this comment to the function:
/// @dev Use with caution! Some functions in this contract knowingly create dirty bits at the destination of the free memory pointer. Note that this code probably isn’t secure or a good use case for assembly because a lot of memory management and security checks are bypassed.
Order of Functions; ordering helps readers identify which functions they can call and to find the constructor and fallback definitions easier. But there are contracts in the project that do not comply with this.
https://docs.soliditylang.org/en/v0.8.17/style-guide.html
Functions should be grouped according to their visibility and ordered:
All in-scope contracts
delete
to Clear Variablesdelete a
assigns the initial value for the type to a
. i.e. for integers it is equivalent to a = 0
, but it can also be used on arrays, where it assigns a dynamic array of length zero or a static array of the same length with all elements reset. For structs, it assigns a struct with all members reset. Similarly, it can also be used to set an address to zero address. It has no effect on whole mappings though (as the keys of mappings may be arbitrary and are generally unknown). However, individual keys and what they map to can be deleted: If a
is a mapping, then delete a[x]
will delete the value stored at x
.
The delete
key better conveys the intention and is also more idiomatic. Consider replacing assignments of zero with delete
statements.
239: advancedOrder.numerator = 0;
It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability.
https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
All in-scope contracts
Include return parameters in NatSpec comments
Recommendation Code Style: (from Uniswap3)
/// @notice Adds liquidity for the given recipient/tickLower/tickUpper position /// @dev The caller of this method receives a callback in the form of IUniswapV3MintCallback#uniswapV3MintCallback /// in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends /// on tickLower, tickUpper, the amount of liquidity, and the current price. /// @param recipient The address for which the liquidity will be created /// @param tickLower The lower tick of the position in which to add liquidity /// @param tickUpper The upper tick of the position in which to add liquidity /// @param amount The amount of liquidity to mint /// @param data Any data that should be passed through to the callback /// @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in the callback /// @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in the callback function mint( address recipient, int24 tickLower, int24 tickUpper, uint128 amount, bytes calldata data ) external returns (uint256 amount0, uint256 amount1);
There is no need to initialize uint
variables to zero as their default value is 0
939: uint256 i = 0;
564: uint256 totalFilteredExecutions = 0;
996: uint256 totalFilteredExecutions = 0;
OpenZeppelin recommends that the initializer modifier be applied to constructors. Per OZs Post implementation contract should be initialized to avoid potential griefs or exploits. https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680/5
93: constructor(address conduitController) Consideration(conduitController)
73: constructor()
31: constructor()
35: constructor( address conduitController ) GettersAndDerivers(conduitController)
41: constructor(address conduitController) OrderValidator(conduitController)
50: constructor(address conduitController) OrderCombiner(conduitController)
56: constructor(address conduitController)
35: constructor(address conduitController) Verifiers(conduitController)
25: constructor( address conduitController ) ConsiderationBase(conduitController)
41: constructor(address conduitController) OrderFulfiller(conduitController)
45: constructor( address conduitController ) BasicOrderFulfiller(conduitController)
55: constructor(address conduitController) Executor(conduitController)
23: constructor()
30: constructor()
26: constructor(address conduitController) Assertions(conduitController)
It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI). It is clearly stated in the Solidity official documentation. In complex projects such as Defi, the interpretation of all functions and their arguments and returns is important for code readability and auditability. https://docs.soliditylang.org/en/v0.8.15/natspec-format.html
All in-scope contracts
NatSpec comments should be increased in contracts
The current form of relative path import is not recommended for use because it can unpredictably pollute the namespace. Instead, the Solidity docs recommend specifying imported symbols explicitly. https://docs.soliditylang.org/en/v0.8.15/layout-of-source-files.html#importing-other-source-files
A good example:
import {OwnableUpgradeable} from "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {SafeCastLib} from "solmate/utils/SafeCastLib.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {IProducer} from "src/interfaces/IProducer.sol"; import {GlobalState, UserState} from "src/Common.sol";
15: import "./lib/ConduitConstants.sol";
8: import "./ConsiderationConstants.sol";
14: import "./ConsiderationErrors.sol";
23: import "./ConsiderationErrors.sol";
23: import "../helpers/PointerLibraries.sol"; 25: import "./ConsiderationConstants.sol";
12: import "./ConsiderationConstants.sol";
20: import "./ConsiderationConstants.sol"; 22: import "../helpers/PointerLibraries.sol";
4: import "./ConsiderationConstants.sol"; 20: import "../helpers/PointerLibraries.sol";
6: import "./ConsiderationConstants.sol";
10: import "./ConsiderationConstants.sol";
15: import "./ConsiderationErrors.sol"; 17: import "../helpers/PointerLibraries.sol";
16: import "./ConsiderationConstants.sol"; 18: import "./ConsiderationErrors.sol";
16: import "./ConsiderationErrors.sol";
8: import "./ConsiderationConstants.sol";
4: import "./ConsiderationConstants.sol";
23: import "./ConsiderationErrors.sol";
23: import "./ConsiderationErrors.sol";
19: import "./ConsiderationErrors.sol";
8: import "./ConsiderationErrors.sol";
12: import "./ConsiderationErrors.sol";
4: import "./TokenTransferrerConstants.sol";
10: import "./ConsiderationErrors.sol";
16: import "./ConsiderationEncoder.sol";
Use specific imports syntax per solidity docs recommendation.
<a href="https://blog.soliditylang.org/2022/03/16/solidity-0.8.13-release-announcement/">0.8.13</a>: Ability to use using for with a list of free functions
<a href="https://blog.soliditylang.org/2022/05/18/solidity-0.8.14-release-announcement/">0.8.14</a>:
ABI Encoder: When ABI-encoding values from calldata that contain nested arrays, correctly validate the nested array length against calldatasize() in all cases. Override Checker: Allow changing data location for parameters only when overriding external functions.
<a href="https://blog.soliditylang.org/2022/06/15/solidity-0.8.15-release-announcement/">0.8.15</a>:
Code Generation: Avoid writing dirty bytes to storage when copying bytes arrays. Yul Optimizer: Keep all memory side-effects of inline assembly blocks.
<a href="https://blog.soliditylang.org/2022/08/08/solidity-0.8.16-release-announcement/">0.8.16</a>:
Code Generation: Fix data corruption that affected ABI-encoding of calldata values represented by tuples: structs at any nesting level; argument lists of external functions, events and errors; return value lists of external functions. The 32 leading bytes of the first dynamically-encoded value in the tuple would get zeroed when the last component contained a statically-encoded array.
<a href="https://blog.soliditylang.org/2022/09/08/solidity-0.8.17-release-announcement/">0.8.17</a>:
Yul Optimizer: Prevent the incorrect removal of storage writes before calls to Yul functions that conditionally terminate the external EVM call.
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
Consider updating to a more recent solidity version.
Throughout the codebase, events are generally emitted when sensitive changes are made to the contracts. However, some events are missing important parameters
51: event PotentialOwnerUpdated(address indexed newPotentialOwner);
The events should include the new value and old value where possible.
87: return amount
902: return orderHash
bytes.concat()
Solidity version 0.8.4 introduces bytes.concat()
(vs abi.encodePacked(<bytes>,<bytes>)
)
76: abi.encodePacked( bytes1(0xff) 339: abi.encodePacked( bytes1(0xff)
177: abi.encodePacked( considerationItemTypeString, offerItemTypeString, orderComponentsPartialTypeString )
Use bytes.concat()
and upgrade to at least Solidity version 0.8.4 if required.
#0 - c4-judge
2023-01-25T09:15:30Z
HickupHH3 marked the issue as grade-b
🌟 Selected for report: Dravee
Also found by: 0xSmartContract, IllIllI, RaymondFam, Rolezn, atharvasama, c3phas, karanctf, saneryee
169.7607 USDC - $169.76
Issue | Contexts | Estimated Gas Saved | |
---|---|---|---|
GAS‑1 | Setting the constructor to payable | 15 | 195 |
GAS‑2 | Using delete statement can save gas | 4 | - |
GAS‑3 | Functions guaranteed to revert when called by normal users can be marked payable | 5 | 105 |
GAS‑4 | Use hardcoded address instead address(this) | 2 | - |
GAS‑5 | internal functions only called once can be inlined to save gas | 1 | - |
GAS‑6 | Optimize names to save gas | 21 | 462 |
GAS‑7 | <x> += <y> Costs More Gas Than <x> = <x> + <y> For State Variables | 25 | - |
GAS‑8 | Help The Optimizer By Saving A Storage Variable’s Reference Instead Of Repeatedly Fetching It | 4 | - |
GAS‑9 | Usage of uints /ints smaller than 32 bytes (256 bits) incurs overhead | 8 | - |
GAS‑10 | Use solidity version 0.8.17 to gain some gas boost | 32 | 2816 |
GAS‑11 | Using storage instead of memory saves gas | 13 | 4550 |
Total: 130 contexts over 11 issues
constructor
to payable
Saves ~13 gas per instance
93: constructor(address conduitController) Consideration(conduitController)
73: constructor()
31: constructor()
35: constructor( address conduitController ) GettersAndDerivers(conduitController)
41: constructor(address conduitController) OrderValidator(conduitController)
50: constructor(address conduitController) OrderCombiner(conduitController)
56: constructor(address conduitController)
35: constructor(address conduitController) Verifiers(conduitController)
25: constructor( address conduitController ) ConsiderationBase(conduitController)
41: constructor(address conduitController) OrderFulfiller(conduitController)
45: constructor( address conduitController ) BasicOrderFulfiller(conduitController)
55: constructor(address conduitController) Executor(conduitController)
23: constructor()
30: constructor()
26: constructor(address conduitController) Assertions(conduitController)
delete
statement can save gas939: uint256 i = 0;
239: advancedOrder.numerator = 0;
564: uint256 totalFilteredExecutions = 0;
996: uint256 totalFilteredExecutions = 0;
payable
If a function modifier or require such as onlyOwner/onlyX is used, the function will revert if a normal user tries to pay the function. Marking the function as payable will lower the gas cost for legitimate callers because the compiler will not include checks for whether a payment was provided. The extra opcodes avoided are CALLVALUE(2), DUP1(3), ISZERO(3), PUSH2(3), JUMPI(10), PUSH1(3), DUP1(3), REVERT(0), JUMPDEST(1), POP(2) which costs an average of about 21 gas per call to the function, in addition to the extra deployment cost.
92: function execute( ConduitTransfer[] calldata transfers ) external override onlyOpenChannel returns (bytes4 magicValue) { 127: function execute( ConduitTransfer[] calldata transfers ) external override onlyOpenChannel returns (bytes4 magicValue) { 154: function execute( ConduitTransfer[] calldata transfers ) external override onlyOpenChannel returns (bytes4 magicValue) {
127: function executeBatch1155( ConduitBatch1155Transfer[] calldata batchTransfers ) external override onlyOpenChannel returns (bytes4 magicValue) {
154: function executeWithBatch1155( ConduitTransfer[] calldata standardTransfers, ConduitBatch1155Transfer[] calldata batchTransfers ) external override onlyOpenChannel returns (bytes4 magicValue) {
Functions guaranteed to revert when called by normal users can be marked payable.
address(this)
Instead of using address(this)
, it is more gas-efficient to pre-calculate and use the hardcoded address
. Foundry's script.sol and solmate's LibRlp.sol
contracts can help achieve this.
References: https://book.getfoundry.sh/reference/forge-std/compute-create-address
https://twitter.com/transmissions11/status/1518507047943245824
78: address(this),
341: address(this),
Use hardcoded address
internal
functions only called once can be inlined to save gas125: function _transferIndividual721Or1155Item
Contracts most called functions could simply save gas by function ordering via Method ID. Calling a function at runtime will be cheaper if the function is positioned earlier in the order (has a relatively lower Method ID) because 22 gas are added to the cost of a function for every position that came before it. The caller can save on gas if you prioritize most called functions.
See more <a href="https://medium.com/joyso/solidity-how-does-function-name-affect-gas-consumption-in-smart-contract-47d270d8ac92">here</a>
All in-scope contracts
Find a lower method ID name for the most called functions for example Call() vs. Call1() is cheaper by 22 gas For example, the function IDs in the Gauge.sol contract will be the most used; A lower method ID may be given.
<x> += <y>
Costs More Gas Than <x> = <x> + <y>
For State Variables941: i += AdditionalRecipient_size 965: nativeTokensRemaining -= additionalRecipientAmount;
1097: amount -= additionalRecipientAmount; 1112: i += AdditionalRecipient_size;
399: for (uint256 offset = 0; offset < tailOffset; offset += OneWord) {
498: for (uint256 offset = 0; offset < tailOffset; offset += OneWord) {
538: for (uint256 offset = 0; offset < tailOffset; offset += OneWord) {
630: for (uint256 offset = 0; offset < tailOffset; offset += OneWord) {
673: for (uint256 offset = 0; offset < tailOffset; offset += OneWord) {
744: for (uint256 offset = 0; offset < tailOffset; offset += OneWord) {
134: tailOffset += minimumReceivedSize; 155: tailOffset += maximumSpentSize; 172: tailOffset += contextSize;
240: tailOffset += offerSize; 259: tailOffset += considerationSize; 273: tailOffset += contextSize; 287: tailOffset += orderHashesSize;
379: tailOffset += offerSize; 400: tailOffset += considerationSize; 413: tailOffset += extraDataSize; 429: tailOffset += orderHashesSize;
514: tailOffset += offerAndConsiderationSize; 523: tailOffset += OneWord;
230: for (uint256 i = 32; i < terminalMemoryOffset; i += 32) { 230: for (uint256 i = 32; i < terminalMemoryOffset; i += 32) {
To help the optimizer, declare a storage type variable and use it instead of repeatedly fetching the reference in a map or an array. The effect can be quite significant. As an example, instead of repeatedly calling someMap[someIndex], save its reference like this: SomeStruct storage someStruct = someMap[someIndex] and use it.
74: AdvancedOrder memory advancedOrder = advancedOrders[orderIndex];
508: contractNonce = _contractNonces[offerer]++;
699: orderStatus = _orderStatus[orderHash];
774: orderStatus = _orderStatus[orderHash];
uints
/ints
smaller than 32 bytes (256 bits) incurs overheadWhen using elements that are smaller than 32 bytes, your contract's gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size.
https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html
Each operation involving a uint8
costs an extra 22-28 gas (depending on whether the other operand is also a variable of type uint8
) as compared to ones involving uint256
, due to the compiler having to clear the higher bits of the memory word before operating on the uint8
, as well as the associated stack operations of doing so. Use a larger size then downcast where needed
192: uint120 numerator;
193: uint120 denominator;
Upgrade to the latest solidity version 0.8.17 to get additional gas savings.
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
pragma solidity ^0.8.13;
storage
instead of memory
saves gasWhen fetching data from a storage
location, assigning the data to a memory
variable causes all fields of the struct/array to be read from storage
, which incurs a Gcoldsload (2100 gas) for each field of the struct/array. If the fields are read from the new memory
variable, they incur an additional MLOAD rather than a cheap stack read. Instead of declearing the variable with the memory keyword, declaring the variable with the storage
keyword and caching any fields that need to be re-read in stack variables, will be much cheaper, only incuring the Gcoldsload for the fields actually read. The only time it makes sense to read the whole struct/array into a memory
variable, is if the full struct/array is being returned by the function, is being passed to a function that requires memory, or if the array/struct is being read from another memory
array/struct
74: AdvancedOrder memory advancedOrder = advancedOrders[orderIndex]; 143: AdvancedOrder memory advancedOrder = advancedOrders[i];
199: OfferItem memory offerItem = offer[componentIndex];
122: FulfillmentComponent memory targetComponent = offerComponents[0];
300: OfferItem memory offerItem = offer[j]; 763: OfferItem memory offerItem = offer[j];
700: Execution memory execution = executions[i]; 738: AdvancedOrder memory advancedOrder = advancedOrders[i]; 300: OfferItem memory offerItem = offer[j]; 763: OfferItem memory offerItem = offer[j];
1001: Fulfillment memory fulfillment = fulfillments[i];
209: OfferItem memory offerItem = orderParameters.offer[i];
755: Order memory order = orders[i];
#0 - c4-judge
2023-01-26T03:57:58Z
HickupHH3 marked the issue as grade-b