Skip to content

Commit

Permalink
feat: use custom ERC20 token to buy and sell NFTs
Browse files Browse the repository at this point in the history
  • Loading branch information
Markkop committed Jan 30, 2022
1 parent d817d12 commit 7c418fa
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 44 deletions.
13 changes: 13 additions & 0 deletions contracts/MarkToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// contracts/OurToken.sol
// SPDX-License-Identifier: MIT
// From: https://docs.openzeppelin.com/contracts/4.x/erc20
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MarkToken is ERC20 {
constructor() ERC20("MARKTOKEN", "MTOKEN") {
uint256 initialSupply = 1000000 * 10**18; // 1,000,00
_mint(msg.sender, initialSupply);
}
}
18 changes: 12 additions & 6 deletions contracts/Marketplace.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./NFT.sol";
import "./MarkToken.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

Expand All @@ -15,7 +16,6 @@ contract Marketplace is ReentrancyGuard {

address payable private owner;

// Challenge: make this price dynamic according to the current currency price
uint256 private listingFee = 0.045 ether;

mapping(uint256 => MarketItem) private marketItemIdToMarketItem;
Expand Down Expand Up @@ -58,11 +58,13 @@ contract Marketplace is ReentrancyGuard {
*/
function createMarketItem(
address nftContractAddress,
address erc20ContractAddress,
uint256 feeAmount,
uint256 tokenId,
uint256 price
) public payable nonReentrant returns (uint256) {
require(price > 0, "Price must be at least 1 wei");
require(msg.value == listingFee, "Price must be equal to listing price");
require(feeAmount == listingFee, "Price must be equal to listing price");
_marketItemIds.increment();
uint256 marketItemId = _marketItemIds.current();

Expand All @@ -80,6 +82,7 @@ contract Marketplace is ReentrancyGuard {
false
);

MarkToken(erc20ContractAddress).transferFrom(msg.sender, address(this), listingFee);
IERC721(nftContractAddress).transferFrom(msg.sender, address(this), tokenId);

emit MarketItemCreated(
Expand Down Expand Up @@ -136,20 +139,23 @@ contract Marketplace is ReentrancyGuard {
* @dev Creates a market sale by transfering msg.sender money to the seller and NFT token from the
* marketplace to the msg.sender. It also sends the listingFee to the marketplace owner.
*/
function createMarketSale(address nftContractAddress, uint256 marketItemId) public payable nonReentrant {
function createMarketSale(
address nftContractAddress,
address erc20ContractAddress,
uint256 marketItemId
) public payable nonReentrant {
uint256 price = marketItemIdToMarketItem[marketItemId].price;
uint256 tokenId = marketItemIdToMarketItem[marketItemId].tokenId;
require(msg.value == price, "Please submit the asking price in order to continue");

marketItemIdToMarketItem[marketItemId].owner = payable(msg.sender);
marketItemIdToMarketItem[marketItemId].sold = true;

marketItemIdToMarketItem[marketItemId].seller.transfer(msg.value);
MarkToken(erc20ContractAddress).transferFrom(msg.sender, marketItemIdToMarketItem[marketItemId].seller, price);
IERC721(nftContractAddress).transferFrom(address(this), msg.sender, tokenId);

_tokensSold.increment();

payable(owner).transfer(listingFee);
MarkToken(erc20ContractAddress).transfer(owner, listingFee);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion pages/api/addresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default function handler (req, res) {
const network = req.query.network
res.status(200).json({
marketplaceAddress: process.env[`MARKETPLACE_CONTRACT_ADDRESS_${network}`],
nftAddress: process.env[`NFT_CONTRACT_ADDRESS_${network}`]
nftAddress: process.env[`NFT_CONTRACT_ADDRESS_${network}`],
erc20Address: process.env[`ERC20_CONTRACT_ADDRESS_${network}`]
})
}
10 changes: 8 additions & 2 deletions scripts/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ const hre = require('hardhat')
const dotenv = require('dotenv')
const fs = require('fs')

function replaceEnvContractAddresses (marketplaceAddress, nftAddress, networkName) {
function replaceEnvContractAddresses (marketplaceAddress, nftAddress, erc20Address, networkName) {
const envFileName = '.env.local'
const envFile = fs.readFileSync(envFileName, 'utf-8')
const env = dotenv.parse(envFile)
env[`MARKETPLACE_CONTRACT_ADDRESS_${networkName}`] = marketplaceAddress
env[`NFT_CONTRACT_ADDRESS_${networkName}`] = nftAddress
env[`ERC20_CONTRACT_ADDRESS_${networkName}`] = erc20Address
const newEnv = Object.entries(env).reduce((env, [key, value]) => {
return `${env}${key}=${value}\n`
}, '')
Expand All @@ -27,7 +28,12 @@ async function main () {
await nft.deployed()
console.log('Nft deployed to:', nft.address)

replaceEnvContractAddresses(marketplace.address, nft.address, hre.network.name.toUpperCase())
const ERC20 = await hre.ethers.getContractFactory('MarkToken')
const erc20 = await ERC20.deploy()
await erc20.deployed()
console.log('Erc20 deployed to:', erc20.address)

replaceEnvContractAddresses(marketplace.address, nft.address, erc20.address, hre.network.name.toUpperCase())
}

main()
Expand Down
44 changes: 32 additions & 12 deletions scripts/setupMarket.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@ async function getCreatedMarketItemId (transaction) {
return value.toNumber()
}

async function setupMarket (marketplaceAddress, nftAddress) {
async function setupMarket (marketplaceAddress, nftAddress, erc20Address) {
const networkName = hre.network.name.toUpperCase()
marketplaceAddress = marketplaceAddress || process.env[`MARKETPLACE_CONTRACT_ADDRESS_${networkName}`]
nftAddress = nftAddress || process.env[`NFT_CONTRACT_ADDRESS_${networkName}`]
erc20Address = erc20Address || process.env[`ERC20_CONTRACT_ADDRESS_${networkName}`]

const marketplaceContract = await hre.ethers.getContractAt('Marketplace', marketplaceAddress)
const nftContract = await hre.ethers.getContractAt('NFT', nftAddress)
const erc20Contract = await hre.ethers.getContractAt('MarkToken', erc20Address)
const nftContractAddress = nftContract.address
const erc20ContractAddress = erc20Contract.address
const [acc1, acc2] = await hre.ethers.getSigners()

await erc20Contract.transfer(acc2.address, hre.ethers.utils.parseEther('500000'))
const price = hre.ethers.utils.parseEther('0.01')
const listingFee = await marketplaceContract.getListingFee()

Expand All @@ -42,11 +46,20 @@ async function setupMarket (marketplaceAddress, nftAddress) {
const codeconTokenId = await getMintedTokenId(codeconMintTx)
const webArMintTx = await nftContract.mintToken(webArMetadataUrl)
const webArTokenId = await getMintedTokenId(webArMintTx)
await marketplaceContract.createMarketItem(nftContractAddress, dogsTokenId, price, { value: listingFee })
await marketplaceContract.createMarketItem(nftContractAddress, techEventTokenId, price, { value: listingFee })
const codeconMarketTx = await marketplaceContract.createMarketItem(nftContractAddress, codeconTokenId, price, { value: listingFee })

await erc20Contract.approve(marketplaceContract.address, listingFee)
await marketplaceContract.createMarketItem(nftContractAddress, erc20ContractAddress, listingFee, dogsTokenId, price)

await erc20Contract.approve(marketplaceContract.address, listingFee)
await marketplaceContract.createMarketItem(nftContractAddress, erc20ContractAddress, listingFee, techEventTokenId, price)

await erc20Contract.approve(marketplaceContract.address, listingFee)
const codeconMarketTx = await marketplaceContract.createMarketItem(nftContractAddress, erc20ContractAddress, listingFee, codeconTokenId, price)
const codeconMarketItemId = await getCreatedMarketItemId(codeconMarketTx)
await marketplaceContract.createMarketItem(nftContractAddress, webArTokenId, price, { value: listingFee })

await erc20Contract.approve(marketplaceContract.address, listingFee)
await marketplaceContract.createMarketItem(nftContractAddress, erc20ContractAddress, listingFee, webArTokenId, price)

console.log(`${acc1.address} minted tokens ${dogsTokenId}, ${techEventTokenId}, ${codeconTokenId} and ${webArTokenId} and listed them as market items`)

await marketplaceContract.cancelMarketItem(nftContractAddress, codeconMarketItemId)
Expand All @@ -56,22 +69,29 @@ async function setupMarket (marketplaceAddress, nftAddress) {
const yellowTokenId = await getMintedTokenId(yellowMintTx)
const ashleyMintTx = await nftContract.connect(acc2).mintToken(ashleyMetadataUrl)
const ashleyTokenId = await getMintedTokenId(ashleyMintTx)
await marketplaceContract.connect(acc2).createMarketItem(nftContractAddress, yellowTokenId, price, { value: listingFee })
await marketplaceContract.connect(acc2).createMarketItem(nftContractAddress, ashleyTokenId, price, { value: listingFee })
await erc20Contract.connect(acc2).approve(marketplaceContract.address, listingFee)
await marketplaceContract.connect(acc2).createMarketItem(nftContractAddress, erc20ContractAddress, listingFee, yellowTokenId, price)
await erc20Contract.connect(acc2).approve(marketplaceContract.address, listingFee)
await marketplaceContract.connect(acc2).createMarketItem(nftContractAddress, erc20ContractAddress, listingFee, ashleyTokenId, price)
console.log(`${acc2.address} minted tokens ${yellowTokenId} and ${ashleyTokenId} and listed them as market items`)

await marketplaceContract.createMarketSale(nftContractAddress, yellowTokenId, { value: price })
await erc20Contract.approve(marketplaceContract.address, price)
await marketplaceContract.createMarketSale(nftContractAddress, erc20ContractAddress, yellowTokenId)
console.log(`${acc1.address} bought token ${yellowTokenId}`)
await nftContract.approve(marketplaceContract.address, yellowTokenId)
await marketplaceContract.createMarketItem(nftContractAddress, yellowTokenId, price, { value: listingFee })
await erc20Contract.approve(marketplaceContract.address, listingFee)
await marketplaceContract.createMarketItem(nftContractAddress, erc20ContractAddress, listingFee, yellowTokenId, price)
console.log(`${acc1.address} put token ${yellowTokenId} for sale`)

await marketplaceContract.connect(acc2).createMarketSale(nftContractAddress, dogsTokenId, { value: price })
await erc20Contract.connect(acc2).approve(marketplaceContract.address, price)
await marketplaceContract.connect(acc2).createMarketSale(nftContractAddress, erc20ContractAddress, dogsTokenId)
await nftContract.connect(acc2).approve(marketplaceContract.address, dogsTokenId)
await marketplaceContract.connect(acc2).createMarketItem(nftContractAddress, dogsTokenId, price, { value: listingFee })
await erc20Contract.connect(acc2).approve(marketplaceContract.address, listingFee)
await marketplaceContract.connect(acc2).createMarketItem(nftContractAddress, erc20ContractAddress, listingFee, dogsTokenId, price)
console.log(`${acc2.address} bought token ${dogsTokenId} and put it for sale`)

await marketplaceContract.connect(acc2).createMarketSale(nftContractAddress, webArTokenId, { value: price })
await erc20Contract.connect(acc2).approve(marketplaceContract.address, price)
await marketplaceContract.connect(acc2).createMarketSale(nftContractAddress, erc20ContractAddress, webArTokenId)
console.log(`${acc2.address} bought token ${webArTokenId}`)
}

Expand Down
20 changes: 11 additions & 9 deletions src/components/molecules/NFTCard.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import { ethers } from 'ethers'
import { ethers, BigNumber } from 'ethers'
import { useContext, useEffect, useState } from 'react'
import { makeStyles } from '@mui/styles'
import { Card, CardActions, CardContent, CardMedia, Button, Divider, Box, CircularProgress } from '@mui/material'
Expand Down Expand Up @@ -61,7 +61,7 @@ async function getAndSetListingFee (marketplaceContract, setListingFee) {

export default function NFTCard ({ nft, action, updateNFT }) {
const { setModalNFT, setIsModalOpen } = useContext(NFTModalContext)
const { nftContract, marketplaceContract, hasWeb3 } = useContext(Web3Context)
const { nftContract, marketplaceContract, erc20Contract, hasWeb3 } = useContext(Web3Context)
const [isHovered, setIsHovered] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [listingFee, setListingFee] = useState('')
Expand Down Expand Up @@ -95,10 +95,10 @@ export default function NFTCard ({ nft, action, updateNFT }) {

async function buyNft (nft) {
const price = ethers.utils.parseUnits(nft.price.toString(), 'ether')
const transaction = await marketplaceContract.createMarketSale(nftContract.address, nft.marketItemId, {
value: price
})
await transaction.wait()
const transaction1 = await erc20Contract.approve(marketplaceContract.address, price)
await transaction1.wait()
const transaction2 = await marketplaceContract.createMarketSale(nftContract.address, erc20Contract.address, nft.marketItemId)
await transaction2.wait()
updateNFT()
}

Expand All @@ -116,10 +116,12 @@ export default function NFTCard ({ nft, action, updateNFT }) {
setPriceError(false)
const listingFee = await marketplaceContract.getListingFee()
const priceInWei = ethers.utils.parseUnits(newPrice, 'ether')
const transaction = await marketplaceContract.createMarketItem(nftContract.address, nft.tokenId, priceInWei, { value: listingFee.toString() })
await transaction.wait()
const transaction1 = await erc20Contract.approve(marketplaceContract.address, listingFee)
await transaction1.wait()
const transaction2 = await marketplaceContract.createMarketItem(nftContract.address, erc20Contract.address, listingFee, nft.tokenId, priceInWei)
await transaction2.wait()
updateNFT()
return transaction
return transaction2
}

function handleCardImageClick () {
Expand Down
6 changes: 6 additions & 0 deletions src/components/providers/Web3Provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Web3Modal from 'web3modal'
import { ethers } from 'ethers'
import NFT from '../../../artifacts/contracts/NFT.sol/NFT.json'
import Market from '../../../artifacts/contracts/Marketplace.sol/Marketplace.json'
import ERC20 from '../../../artifacts/contracts/MarkToken.sol/MarkToken.json'
import axios from 'axios'

const contextDefaultValues = {
Expand All @@ -12,6 +13,7 @@ const contextDefaultValues = {
connectWallet: () => {},
marketplaceContract: null,
nftContract: null,
erc20Contract: null,
isReady: false,
hasWeb3: false
}
Expand All @@ -32,6 +34,7 @@ export default function Web3Provider ({ children }) {
const [balance, setBalance] = useState(contextDefaultValues.balance)
const [marketplaceContract, setMarketplaceContract] = useState(contextDefaultValues.marketplaceContract)
const [nftContract, setNFTContract] = useState(contextDefaultValues.nftContract)
const [erc20Contract, setERC20Contract] = useState(contextDefaultValues.erc20Contract)
const [isReady, setIsReady] = useState(contextDefaultValues.isReady)

useEffect(() => {
Expand Down Expand Up @@ -117,6 +120,8 @@ export default function Web3Provider ({ children }) {
setMarketplaceContract(marketplaceContract)
const nftContract = new ethers.Contract(data.nftAddress, NFT.abi, signer)
setNFTContract(nftContract)
const erc20Contract = new ethers.Contract(data.erc20Address, ERC20.abi, signer)
setERC20Contract(erc20Contract)
return true
}

Expand All @@ -126,6 +131,7 @@ export default function Web3Provider ({ children }) {
account,
marketplaceContract,
nftContract,
erc20Contract,
isReady,
network,
balance,
Expand Down
35 changes: 21 additions & 14 deletions test/marketplace.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { BigNumber } = require('ethers')

describe('Marketplace', function () {
let nftContract
let erc20Contract
let marketplaceContract
let nftContractAddress
let owner
Expand All @@ -16,28 +17,32 @@ describe('Marketplace', function () {

const NFT = await ethers.getContractFactory('NFT')
nftContract = await NFT.deploy(marketplaceContract.address)
await nftContract.deployed();
await nftContract.deployed()

const ERC20 = await ethers.getContractFactory('MarkToken')
erc20Contract = await ERC20.deploy()
await erc20Contract.deployed();

[owner, buyer] = await ethers.getSigners()
nftContractAddress = nftContract.address
}

beforeEach(deployContractsAndSetAddresses)

async function mintTokenAndCreateMarketItem (tokenId, price, transactionOptions, account = owner) {
async function mintTokenAndCreateMarketItem (listingFee, tokenId, price, transactionOptions = {}, account = owner) {
await nftContract.connect(account).mintToken('')
return marketplaceContract.connect(account).createMarketItem(nftContractAddress, tokenId, price, transactionOptions)
await erc20Contract.connect(account).approve(marketplaceContract.address, listingFee)
return marketplaceContract.connect(account).createMarketItem(nftContractAddress, erc20Contract.address, listingFee, tokenId, price, transactionOptions)
}

it('creates a Market Item', async function () {
it.only('creates a Market Item', async function () {
// Arrange
const tokenId = 1
const price = ethers.utils.parseEther('10')
const listingFee = await marketplaceContract.getListingFee()
const transactionOptions = { value: listingFee }

// Act and Assert
await expect(mintTokenAndCreateMarketItem(tokenId, price, transactionOptions))
await expect(mintTokenAndCreateMarketItem(listingFee, tokenId, price))
.to.emit(marketplaceContract, 'MarketItemCreated')
.withArgs(
1,
Expand Down Expand Up @@ -202,22 +207,24 @@ describe('Marketplace', function () {
.to.be.revertedWith('Price must be at least 1 wei')
})

it('creates a Market Sale', async function () {
it.only('creates a Market Sale', async function () {
// Arrange
await erc20Contract.transfer(buyer.address, ethers.utils.parseEther('500000'))

const tokenId = 1
const price = ethers.utils.parseEther('1')
const price = ethers.utils.parseEther('33')
const listingFee = await marketplaceContract.getListingFee()
const transactionOptions = { value: listingFee }

await mintTokenAndCreateMarketItem(tokenId, price, transactionOptions)
const initialOwnerBalance = await owner.getBalance()
await mintTokenAndCreateMarketItem(listingFee, tokenId, price)
const initialOwnerBalance = await erc20Contract.balanceOf(owner.address)

// Act
await marketplaceContract.connect(buyer).createMarketSale(nftContractAddress, 1, { value: price })
await erc20Contract.connect(buyer).approve(marketplaceContract.address, price)
await marketplaceContract.connect(buyer).createMarketSale(nftContractAddress, erc20Contract.address, 1)

// Assert
const expectedOwnerBalance = initialOwnerBalance.add(price).add(listingFee)
expect(await owner.getBalance()).to.equal(expectedOwnerBalance)

expect(await erc20Contract.balanceOf(owner.address)).to.equal(expectedOwnerBalance)
expect(await nftContract.ownerOf(tokenId)).to.equal(buyer.address)
})

Expand Down

0 comments on commit 7c418fa

Please sign in to comment.