Tessera - Versus contest - gzeon's results

The easiest way to collectively buy, own, and govern the NFTs you already know and love.

General Information

Platform: Code4rena

Start Date: 16/12/2022

Pot Size: $32,070 USDC

Total HM: 18

Participants: 5

Period: 3 days

Judge: hickuphh3

Total Solo HM: 5

Id: 195

League: ETH

Tessera

Findings Distribution

Researcher Performance

Rank: 2/5

Findings: 6

Award: $0.00

QA:
grade-b

🌟 Selected for report: 4

🚀 Solo Findings: 1

Findings Information

🌟 Selected for report: Lambda

Also found by: gzeon

Labels

bug
3 (High Risk)
satisfactory
duplicate-7

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2022-12-tessera/blob/f37a11407da2af844bbfe868e1422e3665a5f8e4/src/modules/GroupBuy.sol#L228

Vulnerability details

Impact

On a successful purchase, users can call claim on the GroupBuy contract to mint and refund. The refunded amount is userContributions - sum(minReservePrices*qty) - pendingBalances. In case of the actual purchase price < minReservePrices, the difference is not refunded.

Proof of Concept

In the testClaimSuccess test case, add the following line at the end:

assertEq(address(groupBuy).balance, 0);

Only Alice, Bob and Eve contributed to the GroupBuy, so when they all claim the contract should be left with 0 ether. There are no other way to remove the excess ether in the contract.

Tools Used

Foundry

After purchase, record the actual purchase price. Also need to think about rounding issues.

#0 - c4-judge

2022-12-20T03:01:47Z

HickupHH3 marked the issue as duplicate of #7

#1 - c4-judge

2022-12-20T03:01:52Z

HickupHH3 marked the issue as satisfactory

Findings Information

🌟 Selected for report: gzeon

Also found by: Trust, cccz

Labels

bug
3 (High Risk)
primary issue
satisfactory
selected for report
sponsor confirmed
H-05

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2022-12-tessera/blob/f37a11407da2af844bbfe868e1422e3665a5f8e4/src/seaport/modules/OptimisticListingSeaport.sol#L112-L116

Vulnerability details

Impact

The only check on a new proposal is that it is priced lower than the existing proposal. It does not constrain on the _collateral supplied (except it will revert in _verifyBalance if set to 0). Anyone can block normal proposal creation by creating a proposal with lower price but _collateral == 1. When a high total supply is used, the price of each Rae is negligible and enable an attacker to DOS the protocol.

This violated the prevent a user from holding a vault hostage and never letting the piece be reasonably bought requirement.

Proof of Concept

For any proposal, an attacker can deny it with _collateral = 1 and _price = price - 1

If he do not want the NFT to be sold, he can reject the proposal himself, reseting the contract state.

https://github.com/code-423n4/2022-12-tessera/blob/f37a11407da2af844bbfe868e1422e3665a5f8e4/src/seaport/modules/OptimisticListingSeaport.sol#L112-L116

        // Reverts if price per token is not lower than both the proposed and active listings
        if (
            _pricePerToken >= proposedListing.pricePerToken ||
            _pricePerToken >= activeListings[_vault].pricePerToken
        ) revert NotLower();

Add this test to OptimisticListingSeaport.t.sol:

function testProposeRevertLowerTotalValue() public { uint256 _collateral = 100; uint256 _price = 100; // setup testPropose(_collateral, _price); lowerPrice = pricePerToken - 1; // execute vm.expectRevert(); _propose(eve, vault, 1, lowerPrice, offer); // expect _assertListing(eve, 1, lowerPrice, block.timestamp); _assertTokenBalance(eve, token, tokenId, eveTokenBalance - 1); }

[FAIL. Reason: Call did not revert as expected]

Tools Used

Foundry

Require the total value of the new collateral be greater than the previous. This however still allow a Rae holder with sufficiently large holding to block proposal by creating a new proposal and immediately reject it himself.

#0 - trust1995

2022-12-21T06:55:14Z

imo valid, dup of #42

#1 - IllIllI000

2022-12-22T09:34:10Z

#2 - HickupHH3

2022-12-23T00:32:37Z

Dup of #42

#3 - c4-judge

2022-12-23T00:32:51Z

HickupHH3 marked the issue as primary issue

#4 - c4-judge

2022-12-23T00:33:23Z

HickupHH3 marked the issue as satisfactory

#5 - c4-sponsor

2023-01-04T20:57:20Z

stevennevins marked the issue as sponsor confirmed

#6 - HickupHH3

2023-01-11T15:01:00Z

Best report for foundry POC + the following statement:

This violated the prevent a user from holding a vault hostage and never letting the piece be reasonably bought requirement.

#7 - c4-judge

2023-01-11T15:01:08Z

HickupHH3 marked the issue as selected for report

Findings Information

🌟 Selected for report: gzeon

Also found by: Lambda, Trust

Labels

bug
2 (Med Risk)
downgraded by judge
primary issue
satisfactory
selected for report
M-01

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2022-12-tessera/blob/f37a11407da2af844bbfe868e1422e3665a5f8e4/src/modules/GroupBuy.sol#L186-L188

Vulnerability details

Impact

When _purchaseProof.length == 0, GroupBuy.purchase compare the tokenId with the merkleRoot. This allow any tokenId that match the merkleRoot to be purchased, even if they are not included in the allow list during setup.

https://github.com/code-423n4/2022-12-tessera/blob/f37a11407da2af844bbfe868e1422e3665a5f8e4/src/modules/GroupBuy.sol#L186-L188

        if (_purchaseProof.length == 0) {
            // Hashes tokenId to verify merkle root if proof is empty
            if (bytes32(_tokenId) != merkleRoot) revert InvalidProof();

Proof of Concept

Add the following to GroupBuy.t.sol, it would still revert (since no such nft existed) but not as expected.

// modified from testPurchaseRevertInvalidProof function testPurchaseRevertInvalidTokenIdZeroLength() public { // setup testContributeSuccess(); // exploit uint256 invalidPunkId = uint256(nftMerkleRoot); bytes32[] memory invalidPurchaseProof = new bytes32[](0); // expect vm.expectRevert(INVALID_PROOF_ERROR); // execute _purchase( address(this), currentId, address(punksBuyer), address(punks), invalidPunkId, minValue, purchaseOrder, invalidPurchaseProof ); }

Tools Used

Foundry

Hash the tokenId even if there is only when length is 1

#0 - c4-judge

2022-12-20T02:19:33Z

HickupHH3 changed the severity to 2 (Med Risk)

#1 - c4-judge

2022-12-20T02:20:17Z

HickupHH3 marked the issue as duplicate of #11

#2 - c4-judge

2022-12-20T02:20:21Z

HickupHH3 marked the issue as satisfactory

#3 - HickupHH3

2023-01-11T02:15:43Z

Best report because foundry POC.

#4 - c4-judge

2023-01-11T02:15:47Z

HickupHH3 marked the issue as selected for report

#5 - C4-Staff

2023-01-13T17:05:48Z

liveactionllama marked the issue as not a duplicate

#6 - C4-Staff

2023-01-13T17:05:59Z

liveactionllama marked the issue as primary issue

Findings Information

🌟 Selected for report: gzeon

Labels

bug
2 (Med Risk)
disagree with severity
downgraded by judge
primary issue
selected for report
M-02

Awards

Data not available

External Links

Lines of code

https://github.com/code-423n4/2022-12-tessera/blob/f37a11407da2af844bbfe868e1422e3665a5f8e4/src/seaport/modules/OptimisticListingSeaport.sol#L145

Vulnerability details

Impact

In OptimisticListingSeaport.rejectProposal, it revert if proposedListing.collateral < _amount. An attacker can therefore monitor the mempool, reducing the proposedListing.collateral to _amount - 1 by frontruning the rejectProposal call and delay the rejection. The attacker may even be able to deny the rejection when the deadline passes.

https://github.com/code-423n4/2022-12-tessera/blob/f37a11407da2af844bbfe868e1422e3665a5f8e4/src/seaport/modules/OptimisticListingSeaport.sol#L145

        if (proposedListing.collateral < _amount) revert InsufficientCollateral();

https://github.com/code-423n4/2022-12-tessera/blob/f37a11407da2af844bbfe868e1422e3665a5f8e4/src/seaport/modules/OptimisticListingSeaport.sol#L153

        proposedListing.collateral -= _amount;

Proof of Concept

  1. Attacker propose at 10000 collateral at a very low price
  2. Bob try to reject it by purchasing the 10000 collateral
  3. Attacker see Bob's tx in the mempool, frontrun it to reject 1 unit
  4. The proposedListing.collateral is now 9999
  5. Bob's call reverted
  6. This keep happening until PROPOSAL_PERIOD pass or Bob gave up because of gas paid on failing tx
  7. Attacker buy the NFT at a very low price

When proposedListing.collateral < _amount, set _amount to proposedListing.collateral and refund the excess.

#0 - c4-sponsor

2023-01-04T20:23:07Z

stevennevins marked the issue as disagree with severity

#1 - stevennevins

2023-01-04T20:32:56Z

While annoying, i think this is a Medium severity issue. The attacker has to under price their proposal, defend this under priced proposal from other users and front run each purchase > 1, while selling their Raes at a loss, and it would be relatively costly for griefer to defend their underpriced proposal for the duration of the proposal period. Users could purchase 1 Rae at a time without risk of front running

#2 - HickupHH3

2023-01-11T14:45:19Z

Agree with med severity given the external requirements needed to pull this off.

#3 - c4-judge

2023-01-11T14:45:25Z

HickupHH3 changed the severity to 2 (Med Risk)

#4 - c4-judge

2023-01-11T14:45:36Z

HickupHH3 marked the issue as primary issue

#5 - c4-judge

2023-01-11T14:56:55Z

HickupHH3 marked the issue as selected for report

Findings Information

🌟 Selected for report: IllIllI

Also found by: gzeon

Labels

2 (Med Risk)
partial-50
duplicate-36

Awards

Data not available

External Links

Judge has assessed an item in Issue #22 as M risk. The relevant finding follows:

[NC-2] Return values of approve() not checked

#0 - c4-judge

2022-12-20T09:29:08Z

HickupHH3 marked the issue as duplicate of #36

#1 - c4-judge

2022-12-20T09:29:26Z

HickupHH3 marked the issue as satisfactory

#2 - c4-judge

2022-12-20T09:29:47Z

HickupHH3 marked the issue as selected for report

#3 - IllIllI000

2022-12-21T13:36:14Z

@HickupHH3 This issue was marked as non-critical, and did not elaborate on the consequences of the approval, and how it would lead to loss of funds. Doesn't that mean it should, at most, get reduced credit, rather than being selected for report? The submitter of the QA report even says, Disclaimer: report extended from 4naly3er tool, and the finding is a literal copy-paste from that tool.

It is also worth noting that QA items may be marked as a duplicate of a Medium or High finding without being granted an upgrade, since making the case for how an issue can be exploited, and providing a thorough description and proof of concept, is part of what merits a finding properly earning Medium or High severity https://docs.code4rena.com/roles/judges/how-to-judge-a-contest#upgrading-issues-from-qa-gas-to-medium-high

#4 - HickupHH3

2022-12-21T14:41:20Z

Sry, error on my end. This shouldn't have been selected for the report; should've been #36. Thanks for pointing it out.

#5 - c4-judge

2022-12-21T14:41:27Z

HickupHH3 marked the issue as not selected for report

#6 - c4-judge

2022-12-21T14:41:42Z

HickupHH3 marked the issue as selected for report

#7 - HickupHH3

2022-12-21T14:42:21Z

Ok, it's a bug with the extension. Somehow clicking on the GH link and selecting it as best affects this issue instead of #36.

#8 - c4-judge

2022-12-21T14:43:45Z

HickupHH3 marked the issue as not selected for report

#9 - c4-judge

2022-12-21T14:45:19Z

HickupHH3 marked the issue as partial-50

Findings Information

🌟 Selected for report: Trust

Also found by: gzeon

Labels

2 (Med Risk)
partial-25
duplicate-49

Awards

Data not available

External Links

Judge has assessed an item in Issue #22 as M risk. The relevant finding follows:

minBidPrices is rounded down

#0 - c4-judge

2022-12-20T09:34:55Z

HickupHH3 marked the issue as duplicate of #49

#1 - c4-judge

2022-12-20T09:35:02Z

HickupHH3 marked the issue as partial-25

#2 - HickupHH3

2022-12-20T09:35:38Z

functional vulnerability is mentioned, but lacks the impact described by #49

Findings Information

🌟 Selected for report: IllIllI

Also found by: Lambda, cccz, gzeon

Labels

bug
grade-b
QA (Quality Assurance)
sponsor acknowledged
edited-by-warden
Q-03

Awards

Data not available

External Links

QA Report

Disclaimer: report extended from 4naly3er tool

Low Issues

IssueInstances
L-1Unsafe ERC20 operation(s)1
L-2Contribution to GroupBuy should check against minReservePrices1
L-3Onchain merkle tree generation limited the size of tokenId1

Non Critical Issues

IssueInstances
NC-1Missing checks for address(0) when assigning values to address state variables9
NC-2Return values of approve() not checked1
NC-3Functions not used internally could be marked external11
NC-4minBidPrices is rounded down1
NC-5Member of _tokenIds might not be unique1
NC-6Do not use assert() in production1
NC-7Hardcoded fees1
NC-8Wrong comment1

<a name="L-1"></a>[L-1] Unsafe ERC20 operation(s)

Instances (1):

File: src/seaport/targets/SeaportLister.sol

40:                         IERC20(token).approve(conduit, type(uint256).max);

Link to code

<a name="L-2"></a>[L-2] Contribution to GroupBuy should check against minReservePrices

If the bid is below minReservePrices it will be filtered

Instances (1):

        if (msg.value < _quantity * minBidPrices[_poolId] || _quantity == 0)

<a name="L-3"></a>[L-3] Onchain merkle tree generation limited the size of tokenId

Generate merkle tree onchain is expensive espically when you want to include a large set of value. For example, it is not possible to include all Punk in the tokenId becuase the merkle root is generated onchain. Consider generate it offchain and publish the root.

        bytes32 merkleRoot = (length == 1) ? bytes32(_tokenIds[0]) : _generateRoot(_tokenIds);

Link to code

<a name="NC-1"></a>[NC-1] Missing checks for address(0) when assigning values to address state variables

Instances (9):

File: src/punks/protoforms/PunksMarketBuyer.sol

32:         registry = _registry;

33:         wrapper = _wrapper;

34:         listing = _listing;

Link to code

File: src/seaport/modules/OptimisticListingSeaport.sol

71:         registry = _registry;

72:         seaport = _seaport;

73:         zone = _zone;

75:         supply = _supply;

76:         seaportLister = _seaportLister;

Link to code

File: src/seaport/targets/SeaportLister.sol

20:         conduit = _conduit;

Link to code

<a name="NC-2"></a>[NC-2] Return values of approve() not checked

Not all IERC20 implementations revert() when there's a failure in approve(). The function signature has a boolean return value and they indicate errors that way instead. By not checking the return value, operations that should have marked as failed, may potentially go through without actually approving anything

Instances (1):

File: src/seaport/targets/SeaportLister.sol

40:                         IERC20(token).approve(conduit, type(uint256).max);

Link to code

<a name="NC-3"></a>[NC-3] Functions not used internally could be marked external

Instances (11):

File: src/lib/MinPriorityQueue.sol

27:     function initialize(Queue storage self) public {

36:     function getNumBids(Queue storage self) public view returns (uint256) {

41:     function getMin(Queue storage self) public view returns (Bid storage) {

90:     function delMin(Queue storage self) public returns (Bid memory) {

Link to code

File: src/modules/GroupBuy.sol

346:     function getBidInQueue(uint256 _poolId, uint256 _bidId)

371:     function getNextBidId(uint256 _poolId) public view returns (uint256) {

377:     function getNumBids(uint256 _poolId) public view returns (uint256) {

384:     function getBidQuantity(uint256 _poolId, uint256 _bidId) public view returns (uint256) {

402:     function printQueue(uint256 _poolId) public view {

Link to code

File: src/seaport/modules/OptimisticListingSeaport.sol

218:     function list(address _vault, bytes32[] calldata _listProof) public {

344:     function getPermissions()

Link to code

<a name="NC-4"></a>[NC-4] minBidPrices is rounded down

minBidPrices is rounded down and can be 0, which will cause precision issue

        minBidPrices[currentId] = _initialPrice / _totalSupply;

Link to code

<a name="NC-5"></a>[NC-5] Member of _tokenIds might not be unique

        uint256[] calldata _tokenIds,

Link to code

<a name="NC-6"></a>[NC-6] Do not use assert() in production

It will consume all gas and does not provide a revert string, use require() instead

        assert(ConsiderationInterface(_consideration).validate(_orders));

Link to code

        assert(ConsiderationInterface(_consideration).cancel(_orders));

Link to code

<a name="NC-7"></a>[NC-7] Hardcoded fees

Would require migration if either decided to change fee structure in the future.

        uint256 openseaFees = _listingPrice / 40;
        uint256 tesseraFees = _listingPrice / 20;

Link to code

<a name="NC-8"></a>[NC-8] Wrong comments

Here should be + 2 consideration for the fees

            // 1 Consideration for the listing itself + 1 consideration for the fees

Link to code

#0 - HickupHH3

2022-12-20T09:37:17Z

| L-1 | Unsafe ERC20 operation(s) | 1 |

This could've been a dup of #36 or #37, but I can't discern what the vuln is from the title.

#1 - c4-judge

2022-12-20T09:37:36Z

HickupHH3 marked the issue as grade-b

#2 - c4-sponsor

2023-01-05T18:35:18Z

mehtaculous marked the issue as sponsor acknowledged

Findings Information

🌟 Selected for report: gzeon

Also found by: IllIllI

Labels

bug
G (Gas Optimization)
selected for report
sponsor confirmed
edited-by-warden
G-01

Awards

Data not available

External Links

Gas Optimization Report

Disclaimer: report extended from 4naly3er tool

Gas Optimizations

IssueInstances
GAS-1Use assembly to check for address(0)2
GAS-2Cache array length outside of loop2
GAS-3State variables should be cached in stack variables rather than re-reading them from storage5
GAS-4Use calldata instead of memory for function arguments that do not get mutated5
GAS-5Use Custom Errors2
GAS-6Don't initialize variables with default value2
GAS-7++i costs less gas than i++, especially when it's used in for-loops (--i/i-- too)4
GAS-8Use shift Right/Left instead of division/multiplication if possible5
GAS-9Use != 0 instead of > 0 for unsigned integer comparison5
Gas-10Use unchecked when it is safe?
Gas-11Refund contribution and pending balance in the same call1
Gas-12Generate merkle tree offchain1
Gas-13Pack Bid Struck1
Gas-14Pack Queue Struck1

<a name="GAS-1"></a>[GAS-1] Use assembly to check for address(0)

Saves 6 gas per instance

Instances (2):

File: src/seaport/modules/OptimisticListingSeaport.sol

106:             proposedListings[_vault].proposer == address(0) &&

107:             activeListings[_vault].proposer == address(0)

Link to code

<a name="GAS-2"></a>[GAS-2] Cache array length outside of loop

If not cached, the solidity compiler will always read the length of the array during each iteration. That is, if it is a storage array, this is an extra sload operation (100 additional extra gas for each iteration except for the first) and if it is a memory array, this is an extra mload operation (3 additional gas for each iteration except for the first).

Instances (2):

File: src/lib/MinPriorityQueue.sol

98:         for (uint256 i = 0; i < curUserBids.length; i++) {

Link to code

File: src/seaport/modules/OptimisticListingSeaport.sol

390:             for (uint256 i = 0; i < _offer.length; ++i) {

Link to code

<a name="GAS-3"></a>[GAS-3] State variables should be cached in stack variables rather than re-reading them from storage

The instances below point to the second+ access of a state variable within a function. Caching of a state variable replaces each Gwarmaccess (100 gas) with a much cheaper stack read. Other less obvious fixes/optimizations include having local memory caches of state variable structs, or having local caches of state variable contracts/addresses.

Saves 100 gas per instance

Instances (5):

File: src/modules/GroupBuy.sol

92:         contribute(currentId, _quantity, _raePrice);

Link to code

File: src/punks/protoforms/PunksMarketBuyer.sol

37:         proxy = IWrappedPunk(wrapper).proxyInfo(address(this));

64:         IERC721(wrapper).safeTransferFrom(address(this), vault, tokenId);

Link to code

File: src/seaport/modules/OptimisticListingSeaport.sol

362:             seaportLister,

Link to code

File: src/seaport/targets/SeaportLister.sol

38:                         IERC1155(token).setApprovalForAll(conduit, true);

Link to code

<a name="GAS-4"></a>[GAS-4] Use calldata instead of memory for function arguments that do not get mutated

Mark data types as calldata instead of memory where possible. This makes it so that the data is not automatically loaded into memory. If the data passed into the function does not need to be changed (like updating values in an array), it can be passed in as calldata. The one exception to this is if the argument must later be passed into another function that takes an argument that specifies memory storage.

Instances (5):

File: src/modules/GroupBuy.sol

166:         bytes memory _purchaseOrder,

167:         bytes32[] memory _purchaseProof

Link to code

File: src/punks/protoforms/PunksMarketBuyer.sol

46:     function execute(bytes memory _order) external payable returns (address vault) {

Link to code

File: src/seaport/targets/SeaportLister.sol

26:     function validateListing(address _consideration, Order[] memory _orders) external {

51:     function cancelListing(address _consideration, OrderComponents[] memory _orders) external {

Link to code

<a name="GAS-5"></a>[GAS-5] Use Custom Errors

Source Instead of using error strings, to reduce deployment and runtime cost, you should use Custom Errors. This would save both deployment and runtime cost.

Instances (2):

File: src/lib/MinPriorityQueue.sol

42:         require(!isEmpty(self), "nothing to return");

91:         require(!isEmpty(self), "nothing to delete");

Link to code

<a name="GAS-6"></a>[GAS-6] Don't initialize variables with default value

Instances (2):

File: src/lib/MinPriorityQueue.sol

98:         for (uint256 i = 0; i < curUserBids.length; i++) {

Link to code

File: src/seaport/modules/OptimisticListingSeaport.sol

390:             for (uint256 i = 0; i < _offer.length; ++i) {

Link to code

<a name="GAS-7"></a>[GAS-7] ++i costs less gas than i++, especially when it's used in for-loops (--i/i-- too)

Saves 5 gas per loop

Instances (4):

File: src/lib/MinPriorityQueue.sol

60:                 j++;

77:         insert(self, Bid(self.nextBidId++, owner, price, quantity));

98:         for (uint256 i = 0; i < curUserBids.length; i++) {

Link to code

File: src/modules/GroupBuy.sol

74:         poolInfo[++currentId] = PoolInfo(

Link to code

<a name="GAS-8"></a>[GAS-8] Use shift Right/Left instead of division/multiplication if possible

Instances (5):

File: src/lib/MinPriorityQueue.sol

49:         while (k > 1 && isGreater(self, k / 2, k)) {

50:             exchange(self, k, k / 2);

51:             k = k / 2;

Link to code

File: src/seaport/modules/OptimisticListingSeaport.sol

395:         uint256 openseaFees = _listingPrice / 40;

396:         uint256 tesseraFees = _listingPrice / 20;

Link to code

<a name="GAS-9"></a>[GAS-9] Use != 0 instead of > 0 for unsigned integer comparison

Instances (5):

File: src/modules/GroupBuy.sol

124:         if (fillAtAnyPriceQuantity > 0) {

139:         if (filledQuantity > 0) minReservePrices[_poolId] = getMinPrice(_poolId);

268:         if (pendingBalances[msg.sender] > 0) withdrawBalance();

297:         while (quantity > 0) {

Link to code

File: src/seaport/modules/OptimisticListingSeaport.sol

469:         if (isValidated && !isCancelled && totalFilled > 0 && totalFilled == totalSize) {

Link to code

<a name="GAS-10"></a>[GAS-10] Use unchecked when it is safe

For example, those dealing with eth value will basically never overflow:

        userContributions[_poolId][msg.sender] += msg.value;
        totalContributions[_poolId] += msg.value;

Link to code

            filledQuantities[_poolId] += fillAtAnyPriceQuantity;

Link to code

Also those with explicit check before

                lowestBid.quantity -= quantity;

Link to code

<a name="GAS-11"></a>[GAS-11] Refund contribution and pending balance in the same call

        // Transfers remaining contribution balance back to caller
        payable(msg.sender).call{value: contribution}("");

        // Withdraws pending balance of caller if available
        if (pendingBalances[msg.sender] > 0) withdrawBalance();

Link to code

<a name="GAS-12"></a>[GAS-12] Generate merkle tree offchain

Generate merkle tree onchain is expensive espically when you want to include a large set of value. Consider generating it offchain a publish the root when creating a new pool.

        bytes32 merkleRoot = (length == 1) ? bytes32(_tokenIds[0]) : _generateRoot(_tokenIds);

Link to code

<a name="GAS-13"></a>[GAS-13] Pack Bid Struct

The Bid struct can be packed tighter

struct Bid {
    uint256 bidId;
    address owner;
    uint256 price;
    uint256 quantity;
}

Link to code to

struct Bid {
    uint96 bidId;
    address owner;
    uint256 price;
    uint256 quantity;
}

<a name="GAS-14"></a>[GAS-14] Pack Queue Struct

    struct Queue {
        ///@notice incrementing bid id
        uint256 nextBidId;
        ///@notice array backing priority queue
        uint256[] bidIdList;
        ///@notice total number of bids in queue
        uint256 numBids;
        //@notice map bid ids to bids
        mapping(uint256 => Bid) bidIdToBidMap;
        ///@notice map addreses to bids they own
        mapping(address => uint256[]) ownerToBidIds;
    }

Link to code to

    struct Queue {
        ///@notice incrementing bid id
        uint96 nextBidId;
        ///@notice total number of bids in queue
        uint160 numBids;
        ///@notice array backing priority queue
        uint256[] bidIdList;
        //@notice map bid ids to bids
        mapping(uint256 => Bid) bidIdToBidMap;
        ///@notice map addreses to bids they own
        mapping(address => uint256[]) ownerToBidIds;
    }

#0 - HickupHH3

2022-12-20T09:14:57Z

Since there are only 2 gas opt reports, it's easy to do a direct comparison

Unique to #21

GAS-1 GAS-6 (IIRC N.A. in this case) GAS-9 (IIRC N.A. in this case) GAS-10 GAS-11 GAS-12 GAS-13 / GAS-14

Overlap

GAS-2 == G-07 GAS-3 == G-04 GAS-4 == G-02 GAS-5 == G-14 GAS-7 == G-10 GAS-8 == G-13

Unique #28

G-01 G-03 G-05 G-06 G-09 G-11 G-12

I like that #28 states the amount of gas saved per finding and as a whole. However, this report edges out in gas savings IMO, notably with the struct reorderings + doing merkle proof generation off-chain recommendations.

#1 - c4-judge

2022-12-20T09:15:07Z

HickupHH3 marked the issue as selected for report

#2 - IllIllI000

2022-12-20T09:22:35Z

Just note that the struct reordering is not just re-ordering - the suggestion changes the sizes of the various fields, which changes functionality

#3 - HickupHH3

2022-12-20T09:24:29Z

Yes, am aware a couple of uint have been downcasted; seems reasonably large enough to not be a problem.

#4 - c4-sponsor

2023-01-05T18:33:42Z

mehtaculous marked the issue as sponsor confirmed

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter