Skip to content

Commit b4d6c6c

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

6 files changed

Lines changed: 982 additions & 11 deletions

File tree

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

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)