Platform: Code4rena
Start Date: 08/11/2022
Pot Size: $60,500 USDC
Total HM: 6
Participants: 72
Period: 5 days
Judge: Picodes
Total Solo HM: 2
Id: 178
League: ETH
Rank: 10/72
Findings: 2
Award: $411.01
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: RaymondFam
Also found by: 0x1f8b, 0x52, 0xSmartContract, 0xc0ffEE, 0xhacksmithh, 8olidity, Awesome, BClabs, Bnke0x0, Chom, Deivitto, Hashlock, IllIllI, Josiah, KingNFT, Nyx, R2, ReyAdmirado, Rolezn, SamGMK, Sathish9098, SinceJuly, V_B, Vadis, Waze, a12jmx, adriro, ajtra, aphak5010, bearonbike, bin, brgltd, carlitox477, carrotsmuggler, cccz, ch0bu, chaduke, datapunk, delfin454000, erictee, fatherOfBlocks, fs0c, horsefacts, jayphbee, ktg, ladboy233, pashov, perseverancesuccess, rbserver, ret2basic, tnevler, zaskoh
330.1837 USDC - $330.18
The pragma version used are:
pragma solidity ^0.8.0; pragma solidity ^0.8.14; pragma solidity 0.8.17; pragma solidity >=0.5.0;
Note that mixing pragma is not recommended. Because different compiler versions have different meanings and behaviors, it also significantly raises maintenance costs. As a result, depending on the compiler version selected for any given file, deployed contracts may have security issues.
The minimum required version must be 0.8.17; otherwise, contracts will be affected by the following important bug fixes:
When recovering lost tokens, one is always left in the contract, which produces an underflow if there is no balance, and in the worst case, leaves a token always locked.
I decided to mark this issue as low because the amount of tokens, although this subtraction is not reported in any comment of the code, nor is it justified. But keep in mind that depending on which tokens, a fraction could be of high value.
contract TokenRescuer is OwnableTwoSteps, LowLevelETH, LowLevelERC20Transfer { error InsufficientAmount(); /** * @notice Rescue the contract's trapped ETH * @dev Must be called by the current owner * @param to Send the contract's ETH balance to this address */ function rescueETH(address to) external onlyOwner { - uint256 withdrawAmount = address(this).balance - 1; if (withdrawAmount == 0) revert InsufficientAmount(); _transferETH(to, withdrawAmount); } /** * @notice Rescue any of the contract's trapped ERC20 tokens * @dev Must be called by the current owner * @param currency The address of the ERC20 token to rescue from the contract * @param to Send the contract's specified ERC20 token balance to this address */ function rescueERC20(address currency, address to) external onlyOwner { - uint256 withdrawAmount = IERC20(currency).balanceOf(address(this)) - 1; if (withdrawAmount == 0) revert InsufficientAmount(); _executeERC20DirectTransfer(currency, to, withdrawAmount); } }
Affected source code:
supportsInterface
The EIP-165
standard helps detect that a smart contract implements the expected logic, prevents human error when configuring smart contract bindings, so it is recommended to check that the received argument is a contract and supports the expected interface.
Reference:
Affected source code:
abstract
for base contractsabstract
contracts are contracts that have at least one function without its implementation. An instance of an abstract cannot be created.
Reference:
Affected source code:
The following contracts would be more convenient to be abstract
:
The following contracts would be more convenient to implement as libraries:
It is not good practice to hardcode values, but if you are dealing with addresses much less, these can change between implementations, networks or projects, so it is convenient to remove these values from the source code.
Affected source code:
Use the selector instead of the raw value:
0x1626ba7e
> SignatureChecker.sol:80Use a constant for 10000
in:
I don't see any advantage of having a global isAtomic
argument to all commands, when this task can be done by the caller, it would make more sense to be able to independently execute one of all commands given in isAtomic
way, so that argument should remain to the MakerOrder
structure.
function execute( BasicOrder[] calldata orders, bytes[] calldata ordersExtraData, bytes memory, address recipient, - bool isAtomic, uint256, address ) external payable override {
Affected source code:
In LooksRareAggregator.approve
, the argument order is different than in _executeERC20Approve
, so it is prone to human error.
function approve( - address marketplace, address currency, + address marketplace, uint256 amount ) external onlyOwner { _executeERC20Approve(currency, marketplace, amount); }
Affected source code:
#0 - c4-judge
2022-11-21T19:51:59Z
Picodes marked the issue as grade-a
#1 - 0xhiroshi
2022-11-22T22:27:38Z
#2 - c4-sponsor
2022-11-22T22:30:02Z
0xhiroshi requested judge review
#3 - 0xhiroshi
2022-11-23T09:39:33Z
Actually we are going to reject finding no.5 for "10000", it is a pretty standard basis point number that represents 100% and most protocols we've seen don't need a hard coded value.
80.8321 USDC - $80.83
If a variable is not set/initialized, the default value is assumed (0, false
, 0x0 ... depending on the data type). You are simply wasting gas if you directly initialize it with its default value.
Proof of concept (without optimizations):
pragma solidity 0.8.15; contract TesterA { function testInit() public view returns (uint) { uint256 a = 0; return a; } } contract TesterB { function testNoInit() public view returns (uint) { uint256 a; return a; } }
Gas saving executing: 8 per entry
TesterA.testInit: 21392 TesterB.testNoInit: 21384
Affected source code:
The following structures could be optimized moving the position of certain values in order to save a lot slots:
OrderComponents
in ConsiderationStructs.sol:22-34:struct OrderComponents { address offerer; address zone; + OrderType orderType; OfferItem[] offer; ConsiderationItem[] consideration; - OrderType orderType; uint256 startTime; uint256 endTime; bytes32 zoneHash; uint256 salt; bytes32 conduitKey; uint256 counter; }
BasicOrderParameters
in ConsiderationStructs.sol:100-121struct BasicOrderParameters { // calldata offset address considerationToken; // 0x24 uint256 considerationIdentifier; // 0x44 uint256 considerationAmount; // 0x64 address payable offerer; // 0x84 address zone; // 0xa4 address offerToken; // 0xc4 + BasicOrderType basicOrderType; // 0x124 uint256 offerIdentifier; // 0xe4 uint256 offerAmount; // 0x104 - BasicOrderType basicOrderType; // 0x124 uint256 startTime; // 0x144 uint256 endTime; // 0x164 bytes32 zoneHash; // 0x184 uint256 salt; // 0x1a4 bytes32 offererConduitKey; // 0x1c4 bytes32 fulfillerConduitKey; // 0x1e4 uint256 totalOriginalAdditionalRecipients; // 0x204 AdditionalRecipient[] additionalRecipients; // 0x224 bytes signature; // 0x244 // Total length, excluding dynamic array data: 0x264 (580) }
OrderParameters
in ConsiderationStructs.sol:139-152
struct OrderParameters { address offerer; // 0x00 address zone; // 0x20 + OrderType orderType; // 0x80 OfferItem[] offer; // 0x40 ConsiderationItem[] consideration; // 0x60 - OrderType orderType; // 0x80 uint256 startTime; // 0xa0 uint256 endTime; // 0xc0 bytes32 zoneHash; // 0xe0 uint256 salt; // 0x100 bytes32 conduitKey; // 0x120 uint256 totalOriginalConsiderationItems; // 0x140 // offer.length // 0x160 }
Reference:
Enums are represented by integers; the possibility listed first by 0, the next by 1, and so forth. An enum type just acts like uintN, where N is the smallest legal value large enough to accomodate all the possibilities.
calldata
instead of memory
Some methods are declared as external
but the arguments are defined as memory
instead of as calldata
.
By marking the function as external
it is possible to use calldata
in the arguments shown below and save significant gas.
Recommended changes:
abstract contract TokenReceiver { function onERC721Received( address, address, uint256, bytes calldata ) external pure returns (bytes4) { return this.onERC721Received.selector; } function onERC1155Received( address, address, uint256, uint256, - bytes memory + bytes calldata ) external virtual returns (bytes4) { return this.onERC1155Received.selector; } function onERC1155BatchReceived( address, address, - uint256[] memory, - uint256[] memory, - bytes memory + uint256[] calldata, + uint256[] calldata, + bytes calldata ) external virtual returns (bytes4) { return this.onERC1155BatchReceived.selector; } }
function execute( BasicOrder[] calldata orders, bytes[] calldata ordersExtraData, - bytes memory, + bytes calldata, address recipient, bool isAtomic, uint256, address ) external payable override {
It's possible to avoid decoding logic from the ordersExtraData
in LooksRareProxy.execute
.
function execute( BasicOrder[] calldata orders, - bytes[] calldata ordersExtraData, + OrderExtraData[] calldata ordersExtraData, bytes memory, address recipient, bool isAtomic, uint256, address ) external payable override { if (address(this) != aggregator) revert InvalidCaller(); uint256 ordersLength = orders.length; if (ordersLength == 0 || ordersLength != ordersExtraData.length) revert InvalidOrderLength(); for (uint256 i; i < ordersLength; ) { BasicOrder memory order = orders[i]; - OrderExtraData memory orderExtraData = abi.decode(ordersExtraData[i], (OrderExtraData)); + OrderExtraData memory orderExtraData = ordersExtraData[i];
Affected source code:
It's possible to optimize the storage use of LooksRareAggregator
joining the two mapping related to the proxy use to use only one with an struct.
mapping(address => mapping(bytes4 => bool)) private _proxyFunctionSelectors; mapping(address => FeeData) private _proxyFeeData;
Could be changed to:
struct Proxy { mapping(bytes4 => bool) functionSelectors; FeeData feeData; } mapping(address => Proxy) private _proxy;
Affected source code:
#0 - c4-judge
2022-11-21T19:05:10Z
Picodes marked the issue as grade-b
#1 - 0xhiroshi
2022-11-22T17:55:24Z
isOrderAsk = false
function selector / function selector enabled / fee recipient / fee basis point
in 1 slot and only enable 1 function selector for each proxy.#2 - c4-sponsor
2022-11-22T17:55:29Z
0xhiroshi requested judge review
#3 - 0xhiroshi
2022-12-12T23:11:11Z
@Picodes what's the edict of this report?