Skip to content

Commit c25f2ca

Browse files
feat: Add Marketplace contract
1 parent 6229c92 commit c25f2ca

File tree

6 files changed

+905
-11
lines changed

6 files changed

+905
-11
lines changed

moda-contracts/src/Catalog.sol

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ contract Catalog is ICatalog, AccessControlUpgradeable {
3636
/// @dev releasesOwner => releases
3737
mapping(address => address) _registeredReleasesContracts;
3838
/// @dev release => releaseOwner
39-
mapping(address => address) _registeredReleasesOwners;
39+
mapping(address => address) _registeredReleasesOwner;
4040
/// @dev releaseHash => RegisteredRelease
4141
mapping(bytes32 => RegisteredRelease) _registeredReleases;
4242
/// @dev releases => tokenId => tracks on release
@@ -255,7 +255,7 @@ contract Catalog is ICatalog, AccessControlUpgradeable {
255255
_requireReleasesContractNotRegistered(releases);
256256

257257
$._registeredReleasesContracts[releasesOwner] = releases;
258-
$._registeredReleasesOwners[releases] = releasesOwner;
258+
$._registeredReleasesOwner[releases] = releasesOwner;
259259
emit ReleasesRegistered(releases, releasesOwner);
260260
}
261261

@@ -264,17 +264,17 @@ contract Catalog is ICatalog, AccessControlUpgradeable {
264264
CatalogStorage storage $ = _getCatalogStorage();
265265

266266
_requireReleasesContractIsRegistered(releases);
267-
address releasesOwner = $._registeredReleasesOwners[releases];
267+
address releasesOwner = $._registeredReleasesOwner[releases];
268268
delete $._registeredReleasesContracts[releasesOwner];
269-
delete $._registeredReleasesOwners[releases];
269+
delete $._registeredReleasesOwner[releases];
270270
emit ReleasesUnregistered(releases, releasesOwner);
271271
}
272272

273273
/// @inheritdoc IReleasesRegistration
274274
function getReleasesOwner(address releases) external view returns (address owner) {
275275
CatalogStorage storage $ = _getCatalogStorage();
276276

277-
return $._registeredReleasesOwners[releases];
277+
return $._registeredReleasesOwner[releases];
278278
}
279279

280280
/// @inheritdoc IReleasesRegistration
@@ -488,15 +488,15 @@ contract Catalog is ICatalog, AccessControlUpgradeable {
488488
function _requireReleasesContractIsRegistered(address releases) internal view {
489489
CatalogStorage storage $ = _getCatalogStorage();
490490

491-
if ($._registeredReleasesOwners[releases] == address(0)) {
491+
if ($._registeredReleasesOwner[releases] == address(0)) {
492492
revert ReleasesContractIsNotRegistered();
493493
}
494494
}
495495

496496
function _requireReleasesContractNotRegistered(address releases) internal view {
497497
CatalogStorage storage $ = _getCatalogStorage();
498498

499-
if ($._registeredReleasesOwners[releases] != address(0)) {
499+
if ($._registeredReleasesOwner[releases] != address(0)) {
500500
revert ReleasesContractIsAlreadyRegistered();
501501
}
502502
}

moda-contracts/src/Marketplace.sol

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.21;
3+
4+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6+
import "@openzeppelin/contracts/access/AccessControl.sol";
7+
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
8+
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
9+
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
10+
import "../src/interfaces/IMarketplace.sol";
11+
import "../src/interfaces/Releases/IReleasesRegistration.sol";
12+
13+
/**
14+
* @title Marketplace
15+
* @dev This contract allows buying and selling of Releases and charges a fee on each sale.
16+
*/
17+
contract Marketplace is IMarketplace, ERC1155Holder, ReentrancyGuard, AccessControl {
18+
using SafeERC20 for IERC20;
19+
20+
// State Variables
21+
22+
address _usdc;
23+
address _catalog;
24+
address payable public treasury;
25+
uint256 public treasuryFee;
26+
27+
/// @dev seller => saleId => Sale
28+
mapping(address => Sale[]) private sales;
29+
30+
// Errors
31+
error CannotBeZeroAddress();
32+
error TreasuryFeeCannotBeZero();
33+
error TokenAmountCannotBeZero();
34+
error MaxCountCannotBeZero();
35+
error StartCannotBeAfterEnd(uint256 startTime, uint256 endTime);
36+
error InsufficientSupply(uint256 remainingSupply);
37+
error MaxSupplyReached(uint256 maxSupplyPerWallet);
38+
error SaleNotStarted(uint256 startTime, uint256 currentTime);
39+
error SaleHasEnded(uint256 endTime, uint256 currentTime);
40+
error ReleasesIsNotRegistered();
41+
42+
/**
43+
* @dev Constructor
44+
* @param treasury_ - The address of the organizations treasury
45+
* @param treasuryFee_ - The percentage that will be transferred
46+
* to the treasury on each sale. Based on a denominator of 10_000 e.g. 1000 = 10%
47+
* @param usdc - The USDC token address
48+
* @param catalog - The address of the catalog contract
49+
*/
50+
constructor(address payable treasury_, uint256 treasuryFee_, address usdc, address catalog) {
51+
if (usdc == address(0)) revert CannotBeZeroAddress();
52+
if (treasuryFee_ == 0) revert TreasuryFeeCannotBeZero();
53+
if (treasury_ == address(0)) revert CannotBeZeroAddress();
54+
treasury = treasury_;
55+
treasuryFee = treasuryFee_;
56+
_usdc = usdc;
57+
_catalog = catalog;
58+
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
59+
}
60+
61+
// External Functions
62+
63+
/**
64+
* @inheritdoc IMarketplace
65+
*/
66+
function createSale(
67+
address payable beneficiary,
68+
address releases,
69+
uint256 tokenId,
70+
uint256 amountTotal,
71+
uint256 pricePerToken,
72+
uint256 startAt,
73+
uint256 endAt,
74+
uint256 maxCountPerWallet
75+
) external {
76+
if (IReleasesRegistration(_catalog).getReleasesOwner(releases) == address(0)) {
77+
revert ReleasesIsNotRegistered();
78+
}
79+
if (beneficiary == address(0)) revert CannotBeZeroAddress();
80+
if (amountTotal == 0) revert TokenAmountCannotBeZero();
81+
if (endAt != 0 && startAt > endAt) {
82+
revert StartCannotBeAfterEnd(startAt, endAt);
83+
}
84+
if (maxCountPerWallet == 0) revert MaxCountCannotBeZero();
85+
IERC1155(releases).safeTransferFrom(_msgSender(), address(this), tokenId, amountTotal, "");
86+
87+
sales[_msgSender()].push(
88+
Sale({
89+
releaseOwner: _msgSender(),
90+
beneficiary: beneficiary,
91+
releases: releases,
92+
tokenId: tokenId,
93+
amountRemaining: amountTotal,
94+
amountTotal: amountTotal,
95+
pricePerToken: pricePerToken,
96+
startAt: startAt,
97+
endAt: endAt,
98+
maxCountPerWallet: maxCountPerWallet
99+
})
100+
);
101+
102+
emit SaleCreated(_msgSender(), sales[_msgSender()].length - 1);
103+
}
104+
105+
/**
106+
* @inheritdoc IMarketplace
107+
*/
108+
function purchase(
109+
address seller,
110+
uint256 saleId,
111+
uint256 tokenAmount,
112+
address recipient
113+
) external nonReentrant {
114+
Sale storage sale = _getSaleForPurchase(seller, saleId, tokenAmount);
115+
116+
uint256 totalPrice = sale.pricePerToken * tokenAmount;
117+
uint256 fee = (treasuryFee * totalPrice) / 10_000;
118+
IERC20(_usdc).safeTransferFrom(_msgSender(), address(this), totalPrice);
119+
IERC20(_usdc).transfer(treasury, fee);
120+
IERC20(_usdc).transfer(sale.beneficiary, totalPrice - fee);
121+
122+
_transferTokens(sale.releases, sale.tokenId, tokenAmount, recipient);
123+
124+
sale.amountRemaining -= tokenAmount;
125+
126+
emit Purchase(
127+
sale.releases, sale.tokenId, recipient, seller, saleId, tokenAmount, block.timestamp
128+
);
129+
}
130+
131+
/**
132+
* @inheritdoc IMarketplace
133+
*/
134+
function withdraw(uint256 saleId, uint256 tokenAmount) external nonReentrant {
135+
if (tokenAmount == 0) revert TokenAmountCannotBeZero();
136+
Sale storage sale = sales[_msgSender()][saleId];
137+
if (tokenAmount > sale.amountRemaining) {
138+
revert InsufficientSupply(sale.amountRemaining);
139+
}
140+
_transferTokens(sale.releases, sale.tokenId, tokenAmount, _msgSender());
141+
142+
sale.amountRemaining -= tokenAmount;
143+
144+
emit Withdraw(_msgSender(), saleId, tokenAmount);
145+
}
146+
147+
/**
148+
* @inheritdoc IMarketplace
149+
*/
150+
function getSale(address seller, uint256 saleId) external view returns (Sale memory) {
151+
return sales[seller][saleId];
152+
}
153+
154+
/**
155+
* @inheritdoc IMarketplace
156+
*/
157+
function saleCount(address seller) external view returns (uint256) {
158+
return sales[seller].length;
159+
}
160+
161+
/**
162+
* @inheritdoc IMarketplace
163+
*/
164+
function setTreasuryFee(uint256 newFee) external onlyRole(DEFAULT_ADMIN_ROLE) {
165+
treasuryFee = newFee;
166+
}
167+
168+
/**
169+
* @inheritdoc IMarketplace
170+
*/
171+
function setTreasury(address payable newTreasury) external onlyRole(DEFAULT_ADMIN_ROLE) {
172+
treasury = newTreasury;
173+
}
174+
175+
// Public Functions
176+
177+
/**
178+
* @inheritdoc ERC165
179+
*/
180+
function supportsInterface(bytes4 interfaceId)
181+
public
182+
view
183+
override(AccessControl, ERC1155Holder)
184+
returns (bool)
185+
{
186+
return super.supportsInterface(interfaceId);
187+
}
188+
189+
// Internal Functions
190+
191+
/**
192+
* @dev Verifies the purchase process for a sale
193+
* @param seller - The address of the seller
194+
* @param saleId - The id of the sale
195+
* @param tokenAmount - The amount of tokens to purchase
196+
*/
197+
function _getSaleForPurchase(
198+
address seller,
199+
uint256 saleId,
200+
uint256 tokenAmount
201+
) internal view returns (Sale storage) {
202+
Sale storage sale = sales[seller][saleId];
203+
if (sale.startAt > block.timestamp) {
204+
revert SaleNotStarted(sale.startAt, block.timestamp);
205+
}
206+
if (sale.endAt != 0 && sale.endAt < block.timestamp) {
207+
revert SaleHasEnded(sale.endAt, block.timestamp);
208+
}
209+
if (tokenAmount == 0) revert TokenAmountCannotBeZero();
210+
if (tokenAmount > sale.amountRemaining) {
211+
revert InsufficientSupply(sale.amountRemaining);
212+
}
213+
214+
uint256 buyerBalance = IERC1155(sale.releases).balanceOf(_msgSender(), sale.tokenId);
215+
if ((buyerBalance + tokenAmount) > sale.maxCountPerWallet) {
216+
revert MaxSupplyReached(sale.maxCountPerWallet);
217+
}
218+
return sale;
219+
}
220+
221+
/**
222+
* @dev Transfers Release tokens from the contract to the recipient
223+
* @param releases - The address of the Releases contract
224+
* @param tokenId - The id of the token
225+
* @param tokenAmount - The amount of tokens to transfer
226+
* @param recipient - The address that will receive the Tokens
227+
*/
228+
function _transferTokens(
229+
address releases,
230+
uint256 tokenId,
231+
uint256 tokenAmount,
232+
address recipient
233+
) internal {
234+
IERC1155(releases).safeTransferFrom(address(this), recipient, tokenId, tokenAmount, "");
235+
}
236+
}

moda-contracts/src/Releases.sol

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import {IWithdrawRelease} from "./interfaces/Releases/IWithdrawRelease.sol";
1212
import {ICatalog} from "./interfaces/Catalog/ICatalog.sol";
1313
import {ISplitsFactory} from "./interfaces/ISplitsFactory.sol";
1414

15-
16-
1715
/**
1816
* @title Releases
1917
* @dev This contract allows artists or labels to create their own release tokens.
@@ -78,9 +76,7 @@ contract Releases is
7876
address[] calldata releaseAdmins,
7977
string calldata name_,
8078
string calldata symbol_,
81-
8279
ICatalog catalog,
83-
8480
ISplitsFactory splitsFactory
8581
) external initializer {
8682
__ERC1155_init("");

0 commit comments

Comments
 (0)