|
// SPDX-License-Identifier: Apache-2.0 |
|
pragma solidity ^0.8.18; |
|
|
|
// Fully on-chain, fully customizable, Ethscriptions Marketplace |
|
// Info: ethscriptions.com |
|
// Author: @tunnckoCore / wgw.eth / Sigma Wolf |
|
// Date: June 29, 2023 |
|
|
|
error NotAdmin(); |
|
error NotAdminOrPaymentAddress(); |
|
error InvalidDataLength(); |
|
error AlreadyDeposited(); |
|
error AlreadyListed(uint256 price); |
|
error NotEthscriptionOwner(address owner); |
|
error NotListed(); |
|
error NotEthscriptionLister(address lister); |
|
error NotDepositedEthscription(); |
|
error NotListedEthscription(); |
|
error SellerCannotBuyOwn(); |
|
error PriceMismatch(uint256 askingPrice, uint256 sentValue); |
|
error NotSuchEthscriptionDeposited(); |
|
error NotAvailable(); |
|
error NoFundsToWithdraw(); |
|
error NotEnoughFunds(uint256 balance); |
|
|
|
// error PaymentAddressIsContract(); |
|
|
|
contract OnchainEthscriptionsMarketplace { |
|
address public marketplaceDeployer = msg.sender; |
|
address public marketplaceAdminAddress = msg.sender; |
|
address public marketplacePaymentAddress = msg.sender; |
|
|
|
// Customizable. 200 bps = 2%, 50bps = 0.5%, 4bps = 0.04% |
|
uint256 public marketplaceFeeBps = 150; |
|
uint256 public totalVolume = 0; |
|
uint256 public totalFees = 0; |
|
|
|
struct Ethscription { |
|
bytes32 id; |
|
address owner; |
|
} |
|
|
|
struct Listing { |
|
bytes32 id; |
|
address seller; |
|
address buyer; |
|
uint256 price; |
|
uint256 state; |
|
} |
|
|
|
mapping(bytes32 => Listing) public marketListings; |
|
mapping(bytes32 => Ethscription) public depositedEthscriptions; |
|
|
|
event ethscriptions_protocol_TransferEthscription( |
|
address indexed recipient, |
|
bytes32 indexed ethscriptionId |
|
); |
|
|
|
event ethscriptions_market_EthscriptionDeposited( |
|
bytes32 indexed ethscriptionId, |
|
address indexed depositor |
|
); |
|
|
|
event ethscriptions_market_EthscriptionListed( |
|
bytes32 indexed ethscriptionId, |
|
address indexed seller, |
|
uint256 indexed price |
|
); |
|
|
|
event ethscriptions_market_EthscriptionListingCancelled( |
|
bytes32 indexed ethscriptionId, |
|
address indexed seller |
|
); |
|
|
|
event ethscriptions_market_EthscriptionPurchised( |
|
bytes32 indexed ethscriptionId, |
|
address indexed seller, |
|
address indexed buyer, |
|
uint256 price |
|
); |
|
|
|
event ethscriptions_market_EthscriptionWithdrawn( |
|
bytes32 indexed ethscriptionId, |
|
address indexed owner |
|
); |
|
|
|
event ethscriptions_market_EthscriptionOwnerFixed( |
|
bytes32 indexed ethscriptionId, |
|
address indexed owner |
|
); |
|
|
|
event ethscriptions_market_AdminAddressChanged( |
|
address indexed oldAdminAddress, |
|
address indexed newAdminAddress |
|
); |
|
|
|
event ethscriptions_market_PaymentAddressChanged( |
|
address indexed oldPaymentAddress, |
|
address indexed newPaymentAddress |
|
); |
|
|
|
event ethscriptions_market_FeeBpsChanged( |
|
uint256 oldFeeBps, |
|
uint256 newFeeBps |
|
); |
|
event ethscriptions_market_FundsWithdrawn( |
|
address indexed recipent, |
|
uint256 indexed amount |
|
); |
|
|
|
modifier onlyAdmin() { |
|
if (msg.sender != marketplaceAdminAddress) { |
|
revert NotAdmin(); |
|
} |
|
_; |
|
} |
|
|
|
// Only the admin or the payment address can withdraw funds, |
|
// or fix ethscription owner in case someone else "deposit" it. |
|
modifier onlyAdminOrPaymentAddress() { |
|
if ( |
|
msg.sender != marketplaceAdminAddress && |
|
msg.sender != marketplacePaymentAddress |
|
) { |
|
revert NotAdminOrPaymentAddress(); |
|
} |
|
_; |
|
} |
|
|
|
// Straight from OpenZeppelin. Used to check new payment address. |
|
function isContract(address account) internal view returns (bool) { |
|
// This method relies on extcodesize/address.code.length, which returns 0 |
|
// for contracts in construction, since the code is only stored at the end |
|
// of the constructor execution. |
|
|
|
return account.code.length > 0; |
|
} |
|
|
|
// Depositing of ethscription id, sent through transaction input data |
|
fallback(bytes calldata data) external returns (bytes memory) { |
|
if (data.length != 32) { |
|
revert InvalidDataLength(); |
|
} |
|
|
|
bytes32 potentialHash; |
|
bytes memory memData = data; |
|
assembly { |
|
potentialHash := mload(add(memData, 32)) |
|
} |
|
|
|
onEthscriptionReceived(potentialHash, msg.sender); |
|
|
|
return data; |
|
} |
|
|
|
function onEthscriptionReceived( |
|
bytes32 ethscriptionId, |
|
address depositor |
|
) public { |
|
if (depositedEthscriptions[ethscriptionId].owner != address(0)) { |
|
revert AlreadyDeposited(); |
|
} |
|
|
|
depositedEthscriptions[ethscriptionId] = Ethscription({ |
|
id: ethscriptionId, |
|
owner: depositor |
|
}); |
|
|
|
emit ethscriptions_market_EthscriptionDeposited( |
|
ethscriptionId, |
|
depositor |
|
); |
|
} |
|
|
|
function __fakeDeposit(bytes32 ethscriptionId) public onlyAdmin { |
|
onEthscriptionReceived(ethscriptionId, msg.sender); |
|
} |
|
|
|
// List for Sale |
|
function sell(bytes32 ethscriptionId, uint256 price) public { |
|
if (marketListings[ethscriptionId].state == 1) { |
|
revert AlreadyListed({price: marketListings[ethscriptionId].price}); |
|
} |
|
if (depositedEthscriptions[ethscriptionId].owner == address(0)) { |
|
revert NotSuchEthscriptionDeposited(); |
|
} |
|
if (depositedEthscriptions[ethscriptionId].owner != msg.sender) { |
|
revert NotEthscriptionOwner({ |
|
owner: depositedEthscriptions[ethscriptionId].owner |
|
}); |
|
} |
|
|
|
marketListings[ethscriptionId] = Listing({ |
|
id: ethscriptionId, |
|
seller: msg.sender, |
|
buyer: address(0), |
|
price: price, |
|
state: 1 |
|
}); |
|
|
|
emit ethscriptions_market_EthscriptionListed( |
|
ethscriptionId, |
|
msg.sender, |
|
price |
|
); |
|
} |
|
|
|
// Cancel Listing |
|
function cancel(bytes32 ethscriptionId) public { |
|
if (marketListings[ethscriptionId].state != 1) { |
|
revert NotListed(); |
|
} |
|
if (marketListings[ethscriptionId].seller != msg.sender) { |
|
revert NotEthscriptionLister({ |
|
lister: marketListings[ethscriptionId].seller |
|
}); |
|
} |
|
|
|
delete marketListings[ethscriptionId]; |
|
|
|
emit ethscriptions_market_EthscriptionListingCancelled( |
|
ethscriptionId, |
|
msg.sender |
|
); |
|
} |
|
|
|
// Buy Listed Ethscription |
|
function buy(bytes32 ethscriptionId) public payable { |
|
if (depositedEthscriptions[ethscriptionId].owner == address(0)) { |
|
revert NotDepositedEthscription(); |
|
} |
|
if (marketListings[ethscriptionId].seller == address(0)) { |
|
revert NotListedEthscription(); |
|
} |
|
if (marketListings[ethscriptionId].seller == msg.sender) { |
|
revert SellerCannotBuyOwn(); |
|
} |
|
if (marketListings[ethscriptionId].price != msg.value) { |
|
revert PriceMismatch({ |
|
askingPrice: marketListings[ethscriptionId].price, |
|
sentValue: msg.value |
|
}); |
|
} |
|
|
|
uint256 price = marketListings[ethscriptionId].price; |
|
uint256 fee = computeFee(price); |
|
uint256 amount = price - fee; |
|
|
|
payable(marketListings[ethscriptionId].seller).transfer(amount); |
|
|
|
// NOTE: Update item so there's no need the next owner to |
|
// send the ethscription again to the contract's address. |
|
// So the new owner can just list it for sale. |
|
// depositedEthscriptions[ethscriptionId] = Ethscription({ |
|
// id: ethscriptionId, |
|
// owner: msg.sender |
|
// }); |
|
|
|
// NOTE: Force next owner to deposit again |
|
delete depositedEthscriptions[ethscriptionId]; |
|
|
|
marketListings[ethscriptionId] = Listing({ |
|
id: ethscriptionId, |
|
seller: marketListings[ethscriptionId].seller, |
|
buyer: msg.sender, |
|
price: amount, |
|
state: 2 |
|
}); |
|
|
|
emit ethscriptions_protocol_TransferEthscription( |
|
msg.sender, |
|
ethscriptionId |
|
); |
|
|
|
totalVolume += amount; |
|
totalFees += fee; |
|
|
|
emit ethscriptions_market_EthscriptionPurchised( |
|
ethscriptionId, |
|
marketListings[ethscriptionId].seller, |
|
msg.sender, |
|
amount |
|
); |
|
} |
|
|
|
// x / 100 = 1% |
|
// x / 1000 = 0.1% |
|
// x / 10000 = 0.01% |
|
function computeFee(uint256 amount) public view returns (uint) { |
|
return (amount / 10000) * marketplaceFeeBps; |
|
} |
|
|
|
function withdrawEthscription(bytes32 ethscriptionId) public { |
|
if (depositedEthscriptions[ethscriptionId].owner == address(0)) { |
|
revert NotSuchEthscriptionDeposited(); |
|
} |
|
if (depositedEthscriptions[ethscriptionId].owner != msg.sender) { |
|
revert NotEthscriptionOwner({ |
|
owner: depositedEthscriptions[ethscriptionId].owner |
|
}); |
|
} |
|
|
|
delete depositedEthscriptions[ethscriptionId]; |
|
delete marketListings[ethscriptionId]; |
|
|
|
emit ethscriptions_protocol_TransferEthscription( |
|
msg.sender, |
|
ethscriptionId |
|
); |
|
|
|
emit ethscriptions_market_EthscriptionWithdrawn( |
|
ethscriptionId, |
|
msg.sender |
|
); |
|
} |
|
|
|
function withdrawAllFunds() public onlyAdminOrPaymentAddress { |
|
if (address(this).balance == 0) { |
|
revert NoFundsToWithdraw(); |
|
} |
|
|
|
payable(marketplacePaymentAddress).transfer(address(this).balance); |
|
|
|
emit ethscriptions_market_FundsWithdrawn( |
|
marketplacePaymentAddress, |
|
address(this).balance |
|
); |
|
} |
|
|
|
function withdrawFunds(uint256 amount) public onlyAdminOrPaymentAddress { |
|
if (address(this).balance == 0) { |
|
revert NoFundsToWithdraw(); |
|
} |
|
if (amount > address(this).balance) { |
|
revert NotEnoughFunds({balance: address(this).balance}); |
|
} |
|
|
|
payable(marketplacePaymentAddress).transfer(amount); |
|
|
|
emit ethscriptions_market_FundsWithdrawn( |
|
marketplacePaymentAddress, |
|
amount |
|
); |
|
} |
|
|
|
function setAdminAddress(address adminAddress) public onlyAdmin { |
|
emit ethscriptions_market_AdminAddressChanged( |
|
marketplaceAdminAddress, |
|
adminAddress |
|
); |
|
marketplaceAdminAddress = adminAddress; |
|
} |
|
|
|
function setPaymentAddress(address paymentAddress) public onlyAdmin { |
|
// if (isContract(paymentAddress)) { |
|
// revert PaymentAddressIsContract(); |
|
// } |
|
|
|
emit ethscriptions_market_PaymentAddressChanged( |
|
marketplacePaymentAddress, |
|
paymentAddress |
|
); |
|
marketplacePaymentAddress = paymentAddress; |
|
} |
|
|
|
function setFeeBps(uint96 feeBps) public onlyAdmin { |
|
emit ethscriptions_market_FeeBpsChanged(marketplaceFeeBps, feeBps); |
|
marketplaceFeeBps = feeBps; |
|
} |
|
|
|
// Emergency catch. Allow admin to fix the owner of an ethscription, |
|
// in case not the original owner "deposit" the ethscription. |
|
// Because currently, there's no way to check ownership of ethscription. |
|
function fixEthscriptionOwner( |
|
bytes32 ethscriptionId, |
|
address newOwner |
|
) public onlyAdminOrPaymentAddress { |
|
if (depositedEthscriptions[ethscriptionId].owner == address(0)) { |
|
revert NotSuchEthscriptionDeposited(); |
|
} |
|
|
|
depositedEthscriptions[ethscriptionId] = Ethscription({ |
|
id: ethscriptionId, |
|
owner: newOwner |
|
}); |
|
delete marketListings[ethscriptionId]; |
|
|
|
emit ethscriptions_market_EthscriptionOwnerFixed( |
|
ethscriptionId, |
|
newOwner |
|
); |
|
} |
|
} |