Skip to content

Commit 964ac86

Browse files
Marketplace v3 audit macro fixes (#524)
* re-write marketplacev3 in new dynamic pattern * update tests: direct listings * update tests: english auctions * update tests: offers * update function for access control * update storage position acc erc7201 * Use latest dynamic contracts * Update dynamic-contracts deps * POC test for M-1 * Fix [M-1] Native tokens can get locked when bidding or buying from the marketplace * Fix [L-1] Everyone can send native tokens directly to the marketplace * Fix [L-3] Cannot remove upgradability without revoking all default admins * Fix [Q-8] Incorrect VERSION used * Fix [Q-10] Use ERC721Holder and ERC1155Holder * Fix [Q-11] Avoid code duplication by using _msgSender and _msgData * Fix (code comments) [Q-12] Comply with ERC7201 standard * Fix [G-1] Reorder struct members to save storage slots * Fix [L-2] Missing sanity checks for PrimarySale and PlatformFee recipients * Fix [Q-7] Adhere to naming convention for internal function * Fix comment * Addendum [L-1]: test that only weth can send native token to marketplace --------- Co-authored-by: Yash <kumaryashcse@gmail.com>
1 parent 168b4b1 commit 964ac86

File tree

7 files changed

+178
-69
lines changed

7 files changed

+178
-69
lines changed

contracts/prebuilts/marketplace/IMarketplace.sol

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,22 @@ interface IDirectListings {
5959
* @param startTimestamp The UNIX timestamp at and after which NFTs can be bought from the listing.
6060
* @param endTimestamp The UNIX timestamp at and after which NFTs cannot be bought from the listing.
6161
* @param reserved Whether the listing is reserved to be bought from a specific set of buyers.
62+
* @param status The status of the listing (created, completed, or cancelled).
6263
* @param tokenType The type of token listed (ERC-721 or ERC-1155)
6364
*/
6465
struct Listing {
6566
uint256 listingId;
66-
address listingCreator;
67-
address assetContract;
6867
uint256 tokenId;
6968
uint256 quantity;
70-
address currency;
7169
uint256 pricePerToken;
7270
uint128 startTimestamp;
7371
uint128 endTimestamp;
74-
bool reserved;
72+
address listingCreator;
73+
address assetContract;
74+
address currency;
7575
TokenType tokenType;
7676
Status status;
77+
bool reserved;
7778
}
7879

7980
/// @notice Emitted when a new listing is created.
@@ -268,21 +269,22 @@ interface IEnglishAuctions {
268269
* the current winning bid.
269270
* @param startTimestamp The timestamp at and after which bids can be made to the auction
270271
* @param endTimestamp The timestamp at and after which bids cannot be made to the auction.
272+
* @param status The status of the auction (created, completed, or cancelled).
271273
* @param tokenType The type of NFTs auctioned (ERC-721 or ERC-1155)
272274
*/
273275
struct Auction {
274276
uint256 auctionId;
275-
address auctionCreator;
276-
address assetContract;
277277
uint256 tokenId;
278278
uint256 quantity;
279-
address currency;
280279
uint256 minimumBidAmount;
281280
uint256 buyoutBidAmount;
282281
uint64 timeBufferInSeconds;
283282
uint64 bidBufferBps;
284283
uint64 startTimestamp;
285284
uint64 endTimestamp;
285+
address auctionCreator;
286+
address assetContract;
287+
address currency;
286288
TokenType tokenType;
287289
Status status;
288290
}
@@ -452,17 +454,18 @@ interface IOffers {
452454
* @param currency The currency offered for the NFTs.
453455
* @param totalPrice The total offer amount for the NFTs.
454456
* @param expirationTimestamp The timestamp at and after which the offer cannot be accepted.
457+
* @param status The status of the offer (created, completed, or cancelled).
455458
* @param tokenType The type of token (ERC-721 or ERC-1155) the offer is made for.
456459
*/
457460
struct Offer {
458461
uint256 offerId;
459-
address offeror;
460-
address assetContract;
461462
uint256 tokenId;
462463
uint256 quantity;
463-
address currency;
464464
uint256 totalPrice;
465465
uint256 expirationTimestamp;
466+
address offeror;
467+
address assetContract;
468+
address currency;
466469
TokenType tokenType;
467470
Status status;
468471
}

contracts/prebuilts/marketplace/direct-listings/DirectListingsLogic.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ contract DirectListingsLogic is IDirectListings, ReentrancyGuard, ERC2771Context
285285
if (_currency == CurrencyTransferLib.NATIVE_TOKEN) {
286286
require(msg.value == targetTotalPrice, "Marketplace: msg.value must exactly be the total price.");
287287
} else {
288+
require(msg.value == 0, "Marketplace: invalid native tokens sent.");
288289
_validateERC20BalAndAllowance(buyer, _currency, targetTotalPrice);
289290
}
290291

contracts/prebuilts/marketplace/english-auctions/EnglishAuctionsLogic.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ contract EnglishAuctionsLogic is IEnglishAuctions, ReentrancyGuard, ERC2771Conte
135135
"Marketplace: inactive auction."
136136
);
137137
require(_bidAmount != 0, "Marketplace: Bidding with zero amount.");
138+
require(
139+
_targetAuction.currency == CurrencyTransferLib.NATIVE_TOKEN || msg.value == 0,
140+
"Marketplace: invalid native tokens sent."
141+
);
138142
require(
139143
_bidAmount <= _targetAuction.buyoutBidAmount || _targetAuction.buyoutBidAmount == 0,
140144
"Marketplace: Bidding above buyout price."

contracts/prebuilts/marketplace/entrypoint/MarketplaceV3.sol

Lines changed: 36 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ pragma solidity ^0.8.0;
1616
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
1717
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
1818

19+
import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
20+
import { ERC1155Holder, ERC1155Receiver } from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
21+
1922
// ========== Internal imports ==========
2023
import { BaseRouter, IRouter, IRouterState } from "@thirdweb-dev/dynamic-contracts/src/presets/BaseRouter.sol";
2124
import { ERC165 } from "../../../eip/ERC165.sol";
@@ -42,28 +45,41 @@ contract MarketplaceV3 is
4245
ReentrancyGuardInit,
4346
ERC2771ContextUpgradeable,
4447
RoyaltyPaymentsLogic,
45-
IERC721Receiver,
46-
IERC1155Receiver,
48+
ERC721Holder,
49+
ERC1155Holder,
4750
ERC165
4851
{
49-
/// @dev Only MINTER_ROLE holders can sign off on `MintRequest`s.
52+
/// @dev Only EXTENSION_ROLE holders can perform upgrades.
5053
bytes32 private constant EXTENSION_ROLE = keccak256("EXTENSION_ROLE");
5154

5255
bytes32 private constant MODULE_TYPE = bytes32("MarketplaceV3");
53-
uint256 private constant VERSION = 1;
56+
uint256 private constant VERSION = 3;
57+
58+
/// @dev The address of the native token wrapper contract.
59+
address private immutable nativeTokenWrapper;
5460

5561
/*///////////////////////////////////////////////////////////////
5662
Constructor + initializer logic
5763
//////////////////////////////////////////////////////////////*/
5864

59-
constructor(Extension[] memory _extensions, address _royaltyEngineAddress)
60-
BaseRouter(_extensions)
61-
RoyaltyPaymentsLogic(_royaltyEngineAddress)
65+
/// @dev We accept constructor params as a struct to avoid `Stack too deep` errors.
66+
struct MarketplaceConstructorParams {
67+
Extension[] extensions;
68+
address royaltyEngineAddress;
69+
address nativeTokenWrapper;
70+
}
71+
72+
constructor(MarketplaceConstructorParams memory _params)
73+
BaseRouter(_params.extensions)
74+
RoyaltyPaymentsLogic(_params.royaltyEngineAddress)
6275
{
76+
nativeTokenWrapper = _params.nativeTokenWrapper;
6377
_disableInitializers();
6478
}
6579

66-
receive() external payable {}
80+
receive() external payable {
81+
assert(msg.sender == nativeTokenWrapper); // only accept ETH via fallback from the native token wrapper contract
82+
}
6783

6884
/// @dev Initializes the contract, like a constructor.
6985
function initialize(
@@ -88,6 +104,9 @@ contract MarketplaceV3 is
88104
_setupRole(EXTENSION_ROLE, _defaultAdmin);
89105
_setupRole(keccak256("LISTER_ROLE"), address(0));
90106
_setupRole(keccak256("ASSET_ROLE"), address(0));
107+
108+
_setupRole(EXTENSION_ROLE, _defaultAdmin);
109+
_setRoleAdmin(EXTENSION_ROLE, EXTENSION_ROLE);
91110
}
92111

93112
/*///////////////////////////////////////////////////////////////
@@ -108,36 +127,13 @@ contract MarketplaceV3 is
108127
ERC 165 / 721 / 1155 logic
109128
//////////////////////////////////////////////////////////////*/
110129

111-
function onERC1155Received(
112-
address,
113-
address,
114-
uint256,
115-
uint256,
116-
bytes memory
117-
) public virtual override returns (bytes4) {
118-
return this.onERC1155Received.selector;
119-
}
120-
121-
function onERC1155BatchReceived(
122-
address,
123-
address,
124-
uint256[] memory,
125-
uint256[] memory,
126-
bytes memory
127-
) public virtual override returns (bytes4) {
128-
return this.onERC1155BatchReceived.selector;
129-
}
130-
131-
function onERC721Received(
132-
address,
133-
address,
134-
uint256,
135-
bytes calldata
136-
) external pure override returns (bytes4) {
137-
return this.onERC721Received.selector;
138-
}
139-
140-
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
130+
function supportsInterface(bytes4 interfaceId)
131+
public
132+
view
133+
virtual
134+
override(ERC165, IERC165, ERC1155Receiver)
135+
returns (bool)
136+
{
141137
return
142138
interfaceId == type(IERC1155Receiver).interfaceId ||
143139
interfaceId == type(IERC721Receiver).interfaceId ||
@@ -177,21 +173,10 @@ contract MarketplaceV3 is
177173
}
178174

179175
function _msgSender() internal view override(ERC2771ContextUpgradeable, Permissions) returns (address sender) {
180-
if (isTrustedForwarder(msg.sender)) {
181-
// The assembly code is more direct than the Solidity version using `abi.decode`.
182-
assembly {
183-
sender := shr(96, calldataload(sub(calldatasize(), 20)))
184-
}
185-
} else {
186-
return msg.sender;
187-
}
176+
return ERC2771ContextUpgradeable._msgSender();
188177
}
189178

190179
function _msgData() internal view override(ERC2771ContextUpgradeable, Permissions) returns (bytes calldata) {
191-
if (isTrustedForwarder(msg.sender)) {
192-
return msg.data[:msg.data.length - 20];
193-
} else {
194-
return msg.data;
195-
}
180+
return ERC2771ContextUpgradeable._msgData();
196181
}
197182
}

src/test/marketplace/DirectListings.t.sol

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ contract MarketplaceDirectListingsTest is BaseTest, IExtension {
3434

3535
// Deploy implementation.
3636
Extension[] memory extensions = _setupExtensions();
37-
address impl = address(new MarketplaceV3(extensions, address(0)));
37+
address impl = address(
38+
new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth)))
39+
);
3840

3941
vm.prank(marketplaceDeployer);
4042
marketplace = address(
@@ -1865,6 +1867,38 @@ contract MarketplaceDirectListingsTest is BaseTest, IExtension {
18651867
vm.prank(_seller);
18661868
listingId = DirectListingsLogic(marketplace).createListing(listingParams);
18671869
}
1870+
1871+
function test_audit_native_tokens_locked() public {
1872+
(uint256 listingId, IDirectListings.Listing memory existingListing) = _setup_buyFromListing();
1873+
1874+
uint256[] memory tokenIds = new uint256[](1);
1875+
tokenIds[0] = existingListing.tokenId;
1876+
1877+
// Verify existing auction at `auctionId`
1878+
assertEq(existingListing.assetContract, address(erc721));
1879+
1880+
vm.warp(existingListing.startTimestamp);
1881+
1882+
// No ether is locked in contract
1883+
assertEq(marketplace.balance, 0);
1884+
1885+
// buy from listing
1886+
erc20.mint(buyer, 10 ether);
1887+
vm.deal(buyer, 1 ether);
1888+
1889+
vm.prank(seller);
1890+
DirectListingsLogic(marketplace).approveBuyerForListing(listingId, buyer, true);
1891+
1892+
vm.startPrank(buyer);
1893+
erc20.approve(marketplace, 10 ether);
1894+
1895+
vm.expectRevert("Marketplace: invalid native tokens sent.");
1896+
DirectListingsLogic(marketplace).buyFromListing{ value: 1 ether }(listingId, buyer, 1, address(erc20), 1 ether);
1897+
vm.stopPrank();
1898+
1899+
// 1 ether is temporary locked in contract
1900+
assertEq(marketplace.balance, 0 ether);
1901+
}
18681902
}
18691903

18701904
contract IssueC2_MarketplaceDirectListingsTest is BaseTest, IExtension {
@@ -1885,7 +1919,9 @@ contract IssueC2_MarketplaceDirectListingsTest is BaseTest, IExtension {
18851919

18861920
// Deploy implementation.
18871921
Extension[] memory extensions = _setupExtensions();
1888-
address impl = address(new MarketplaceV3(extensions, address(0)));
1922+
address impl = address(
1923+
new MarketplaceV3(MarketplaceV3.MarketplaceConstructorParams(extensions, address(0), address(weth)))
1924+
);
18891925

18901926
vm.prank(marketplaceDeployer);
18911927
marketplace = address(

0 commit comments

Comments
 (0)