Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use custom ERC20 token to buy and sell NFTs #2

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
18 changes: 10 additions & 8 deletions src/components/molecules/NFTCard.js
Original file line number Diff line number Diff line change
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