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: 14/72
Findings: 1
Award: $330.18
🌟 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
Some contract code uses the block.timestamp as part of the calculations and time checks. Nevertheless, timestamps can be slightly altered by miners/validators to favor them in contracts that have logic that depends strongly on them.
Consider taking into account this issue and warning the users that such a scenario could happen. If the alteration of timestamps cannot affect the protocol in any way, consider documenting the reasoning and writing tests enforcing that these guarantees will be preserved even if the code changes in the future. Here are the two instances found.
if (block.timestamp < earliestOwnershipRenouncementTime) revert RenouncementTooEarly();
earliestOwnershipRenouncementTime = block.timestamp + delay;
Try limit the length of comments and/or code lines to 80 - 100 character long for readability sake. There are two instances found.
SeaportProxy.sol: Line 35 - 37
It is a good practice giving time to users to react and adjust to critical changes with a mandatory time window between the changes. The first step is simply broadcasting to users with a specific change that is coming whilst the second step commits that change after an appropriate period of waiting. This would allow time for users opposing to the change to withdraw within the set time frame. A timelock provides more guarantees and reduces the level of trust required, thus decreasing risk for users. It also indicates that the project is legitimate (less risk of the owner making a malicious act). Specifically, privileged roles could use front running to make malicious changes just ahead of incoming transactions, or purely accidental negative effects could occur due to the unfortunate timing of changes.
It is recommended extending the timelock feature to all business critical functions instead of just the contract ownership management. Here are the three instances found.
function addFunction(address proxy, bytes4 selector) external onlyOwner {
function removeFunction(address proxy, bytes4 selector) external onlyOwner {
LooksRareAggregator: Lines 153 - 157
function setFee( address proxy, uint256 bp, address recipient ) external onlyOwner {
As documented in the link below:
https://docs.soliditylang.org/en/v0.8.16/natspec-format.html
It is recommended using a special form of comments, i.e., the Ethereum Natural Language Specification Format (NatSpec) to provide rich documentation for functions, return variables and more.
Here are some of the instances found.
LooksRareAggregator.sol: Lines 220 - 251
receive() external payable {} function _encodeCalldataAndValidateFeeBp( TradeData calldata singleTradeData, address recipient, bool isAtomic ) private view returns (bytes memory proxyCalldata, bool maxFeeBpViolated) { FeeData memory feeData = _proxyFeeData[singleTradeData.proxy]; maxFeeBpViolated = singleTradeData.maxFeeBp < feeData.bp; proxyCalldata = abi.encodeWithSelector( singleTradeData.selector, singleTradeData.orders, singleTradeData.ordersExtraData, singleTradeData.extraData, recipient, isAtomic, feeData.bp, feeData.recipient ); } function _returnERC20TokensIfAny(TokenTransfer[] calldata tokenTransfers, address recipient) private { uint256 tokenTransfersLength = tokenTransfers.length; for (uint256 i; i < tokenTransfersLength; ) { uint256 balance = IERC20(tokenTransfers[i].currency).balanceOf(address(this)); if (balance > 0) _executeERC20DirectTransfer(tokenTransfers[i].currency, recipient, balance); unchecked { ++i; } } }
LooksRareProxy.sol: Lines 107 - 134
function _executeSingleOrder( OrderTypes.TakerOrder memory takerBid, OrderTypes.MakerOrder memory makerAsk, address recipient, CollectionType collectionType, bool isAtomic ) private { if (isAtomic) { marketplace.matchAskWithTakerBidUsingETHAndWETH{value: takerBid.price}(takerBid, makerAsk); _transferTokenToRecipient( collectionType, recipient, makerAsk.collection, makerAsk.tokenId, makerAsk.amount ); } else { try marketplace.matchAskWithTakerBidUsingETHAndWETH{value: takerBid.price}(takerBid, makerAsk) { _transferTokenToRecipient( collectionType, recipient, makerAsk.collection, makerAsk.tokenId, makerAsk.amount ); } catch {} } }
Zero address checks implemented at the constructor could avoid human errors leading to non-functional calls associated with the mistakes. This is especially so when the incidents entail immutable variables preventing them from getting reassigned that could end up having the protocol redeploy the contract.
Here are the three instances found.
LooksRareProxy.sol: Lines 37 - 40
constructor(address _marketplace, address _aggregator) { marketplace = ILooksRareExchange(_marketplace); aggregator = _aggregator; }
SeaportProxy.sol: Lines 45 - 48
constructor(address _marketplace, address _aggregator) { marketplace = SeaportInterface(_marketplace); aggregator = _aggregator; }
ERC20EnabledLooksRareAggregator.sol: Lines 21 - 23
constructor(address _aggregator) { aggregator = ILooksRareAggregator(_aggregator); }
Non-descriptive local variables could make code base difficult to read and navigate. Here are some of the instance found.
ILooksRareAggregator.sol: Line 44
@ `bp` should be changed to something like `feeBasisPoint` event FeeUpdated(address proxy, uint256 bp, address recipient);
@ `bp` should be changed to something like `feeBasisPoint` uint256 bp; // Aggregator fee basis point
Unbounded loop could lead to OOG (Out of Gas) denying the users' of needed services. Here are some instances found.
for (uint256 i; i < ordersLength; ) {
for (uint256 j; j < recipientsLength; ) {
ERC20EnabledLooksRareAggregator.sol: Line 41
for (uint256 i; i < tokenTransfersLength; ) {
ConsiderationStructs.sol: Line 168
@ are should be corrected to is * type is restricted and the offerer or zone are not the caller.
Non-library contracts and interfaces should avoid using floating pragmas ^0.8.9. Doing this may be a security risk for the actual application implementation itself. For instance, a known vulnerable compiler version may accidentally be selected or a security tool might fallback to an older compiler version leading to checking a different EVM compilation that is ultimately deployed on the blockchain.
Here are the contract instances found using ^0.8.0
.
IERC20.sol IERC721.sol IERC1155.sol
Here are the contract instances found using ^0.8.14
.
LowLevelETH.sol LowLevelERC1155Transfer.sol LowLevelERC721Transfer.sol LowLevelERC20Transfer.sol LowLevelERC20Approve.sol SignatureChecker.sol OwnableTwoSteps.sol ReentrancyGuard.sol
Up to three event parameters should be indexed. This will help filter off the logs in listening for specifically wanted data. Using indexed
has the benefit of making the arguments log topics instead of data.
Here is one of the instances found.
ILooksRareAggregator.sol: Line 44
event FeeUpdated(address proxy, uint256 bp, address recipient);
It is recommended having events associated with setter functions emit both the new and old values instead of just the new value. Here is one of the instances found.
LooksRareAggregator.sol: Line 162
emit FeeUpdated(proxy, bp, recipient);
The following event is empty in the parameter field making it incapable of emitting anything. It is recommended removing these unusable lines of code.
ILooksRareAggregator.sol: Line 36
event ERC20EnabledLooksRareAggregatorSet();
LooksRareAggregator.sol: Line 123
emit ERC20EnabledLooksRareAggregatorSet();
Whenever possible, use OpenZeppelin’s ECDSA library which has been time tested in preventing replay attack of signature malleability associated with `ecrecover'. The issue could arise from the non-unique value of v and an s value that could end up in the upper half of the modulo set.
Here is the contract instance found.
signer = ecrecover(hash, v, r, s);
Contract owner possesses numerous privileges that could prove disastrous if the private key was compromised. If the key was lost/unrecoverable, all needed system parameter updates would be halted.
An established owner is generally prone to target attack, especially given the insufficient protection on sensitive owner private keys. The concentration of privileges creates a single point of failure, leading to transfer of ownership and malicious reset of setter function parameters.
The following measures are recommended.
Users could input edge values, e.g. 2**118
and 2**119
as numerator and denominator for an intended simplified fraction of 1/2
. It was a previous bug in the Opensea contract that could cause both numerator and denominator revert to zero/overflow. This could be prevented by making sure that those two inputs could not be more than the total ERC721/ERC1155 collection availability. Multiply them with a factor to remove the decimals if need be. Here are the two for loop instances entailed before calling marketplace.fulfillAvailableAdvancedOrders()
and marketplace.fulfillAdvancedOrder()
:
for (uint256 i; i < ordersLength; ) { OrderExtraData memory orderExtraData = abi.decode(ordersExtraData[i], (OrderExtraData)); advancedOrders[i].parameters = _populateParameters(orders[i], orderExtraData); advancedOrders[i].numerator = orderExtraData.numerator; advancedOrders[i].denominator = orderExtraData.denominator; advancedOrders[i].signature = orders[i].signature; if (orders[i].currency == address(0)) ethValue += orders[i].price; unchecked { ++i; } }
for (uint256 i; i < orders.length; ) { OrderExtraData memory orderExtraData = abi.decode(ordersExtraData[i], (OrderExtraData)); AdvancedOrder memory advancedOrder; advancedOrder.parameters = _populateParameters(orders[i], orderExtraData); advancedOrder.numerator = orderExtraData.numerator; advancedOrder.denominator = orderExtraData.denominator; advancedOrder.signature = orders[i].signature;
#0 - c4-judge
2022-11-21T17:04:52Z
Picodes marked the issue as grade-a
#1 - 0xhiroshi
2022-11-24T23:02:19Z
Most issues addressed in other issues.
For "SEAPORT FUNCTION CALLS", we are passing the validation responsibility to Seaport.
#2 - c4-sponsor
2022-11-24T23:02:53Z
0xhiroshi marked the issue as sponsor disputed