Skip to content

Instantly share code, notes, and snippets.

@tunnckoCore
Last active July 10, 2023 16:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tunnckoCore/ae220662c861167fbac95a9a3e2bff7a to your computer and use it in GitHub Desktop.
Save tunnckoCore/ae220662c861167fbac95a9a3e2bff7a to your computer and use it in GitHub Desktop.
Fully customizable on-chain Ethscriptions Marketplace in 350 lines of Solidity!

On-chain Ethscriptions Marketplace

Fully customizable and fully on-chain Ethscriptions Marketplace in 350 lines of Solidity! Royalties coming soon too.

Ethscriptions are a new way of creating and sharing digital artifacts on Ethereum using transaction calldata, in form of hex input data. Read more about them at https://ethscriptions.com/about.

How the marketplace works?

The marketplace is ESIP-1 compatible contract that works as middleman between a seller and a buyer.

From seller's perspective:

  • Transfer (deposit) ethscription to the contract address - standard Ethscription Transfer (eg. sending the ethscription id in the hex data field to a recipient).
  • Use sell function to list an ethscription for sale, by passing ethscription id and a price
  • Wait for a buyer

From buyer's perspective:

  • Choose an ethscription you want to buy.
  • Use the buy function, by passing the ethscription id to buy and send the asked price in ETH.
  • The contract send the ethscription from the seller to the buyer, and deduct 2% marketplace fee from the sale price, the rest sends to the seller

Other notes:

  • The seller has the option to cancel the listing by calling the cancel function with the ethscription id - this does not withdraw the ethscription, it just cancels the listing
  • The seller can also withdraw an ethscription at any time by using the withdrawEthscription function - this means the contract transfers back the ethscription to you, through ethscriptions_protocol_TransferEthscription

Depositing & Safety

Depositing ethscription means the contract becomes the new owner of that ethscription. That may seem scary, but the contract does nothing malicious, and there's no other way to do trading currently. That requirement is because to be able an Ethscription Transfer to be considered valid (by the indexer / protocol) it should be done by the owner of the ethscription.

Thus, the contract must be an owner first and when a buyer buys a given ethscription, the contract transfers it (using ESIP-1, emitting ethscriptions_protocol_TransferEthscription event), the buyer becomes the new owner, and the seller gets the funds.

Note: Another important thing to note is that currently there's no way to validate if a depositor is actually owning the ethscription it deposits. So, because of that, temporarily an "admin" role can "fix" a deposited ethscription with the correct owner.

Marketplace customization & roles

LIVE DEMO ON GOERLI: https://goerli.etherscan.io/address/0x2238909cEDB338158a63C0B8A7680454Cdc782F9

There are 2 important roles: an ADMIN and PAYMENT_ADDRESS. The default marketplace fees are 2%.

The ADMIN role can:

  • withdraw all funds from fees - withdrawAllFunds
  • withdraw part of the funds - withdrawFunds(amount)
  • "fix" ethscription owner - fixEthscriptionOwner
  • change the fees percentage - setFeeBps, 200 means 2%, 50 = 0.5%, 4 = 0.04%
  • change the admin (address) - eg. can set it to 0x0 so no one can change parameters anymore
  • change the payment address - which must be an EOA (user account and not a contract)

The "PAYMENT address" role:

  • withdraw all funds
  • withdraw part of the funds
  • fix ethscription owner

Initially the the contract deployer is both the admin and funds receiver.

// 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
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment