From 98cc6dc948c03ebf6af0f32609ed516946a61e16 Mon Sep 17 00:00:00 2001 From: Marcelo Kopmann Date: Sun, 30 Jan 2022 15:53:27 -0300 Subject: [PATCH 1/2] feat: use custom ERC20 token to buy and sell NFTs --- contracts/MarkToken.sol | 13 +++++++ contracts/Marketplace.sol | 18 ++++++---- pages/api/addresses.js | 3 +- scripts/deploy.js | 10 ++++-- scripts/setupMarket.js | 44 +++++++++++++++++------- src/components/molecules/NFTCard.js | 20 ++++++----- src/components/providers/Web3Provider.js | 6 ++++ test/marketplace.js | 35 +++++++++++-------- 8 files changed, 105 insertions(+), 44 deletions(-) create mode 100644 contracts/MarkToken.sol diff --git a/contracts/MarkToken.sol b/contracts/MarkToken.sol new file mode 100644 index 0000000..2fdeee8 --- /dev/null +++ b/contracts/MarkToken.sol @@ -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); + } +} diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index 72a9848..2e17fe8 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -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"; @@ -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; @@ -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(); @@ -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( @@ -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); } /** diff --git a/pages/api/addresses.js b/pages/api/addresses.js index 2717551..06162df 100644 --- a/pages/api/addresses.js +++ b/pages/api/addresses.js @@ -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}`] }) } diff --git a/scripts/deploy.js b/scripts/deploy.js index 723a94b..4c4f7cb 100644 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -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` }, '') @@ -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() diff --git a/scripts/setupMarket.js b/scripts/setupMarket.js index 004b914..040aef7 100644 --- a/scripts/setupMarket.js +++ b/scripts/setupMarket.js @@ -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() @@ -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) @@ -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}`) } diff --git a/src/components/molecules/NFTCard.js b/src/components/molecules/NFTCard.js index 57efaae..1b1fe60 100644 --- a/src/components/molecules/NFTCard.js +++ b/src/components/molecules/NFTCard.js @@ -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' @@ -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('') @@ -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() } @@ -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 () { diff --git a/src/components/providers/Web3Provider.js b/src/components/providers/Web3Provider.js index 96d2437..e6ab598 100644 --- a/src/components/providers/Web3Provider.js +++ b/src/components/providers/Web3Provider.js @@ -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 = { @@ -12,6 +13,7 @@ const contextDefaultValues = { connectWallet: () => {}, marketplaceContract: null, nftContract: null, + erc20Contract: null, isReady: false, hasWeb3: false } @@ -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(() => { @@ -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 } @@ -126,6 +131,7 @@ export default function Web3Provider ({ children }) { account, marketplaceContract, nftContract, + erc20Contract, isReady, network, balance, diff --git a/test/marketplace.js b/test/marketplace.js index cd494f4..2efc9e3 100644 --- a/test/marketplace.js +++ b/test/marketplace.js @@ -4,6 +4,7 @@ const { BigNumber } = require('ethers') describe('Marketplace', function () { let nftContract + let erc20Contract let marketplaceContract let nftContractAddress let owner @@ -16,7 +17,11 @@ 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 @@ -24,20 +29,20 @@ describe('Marketplace', function () { 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, @@ -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) }) From f9a246938aeec71be02323ef6fdb274693bc3a03 Mon Sep 17 00:00:00 2001 From: Marcelo Kopmann Date: Sun, 30 Jan 2022 16:08:43 -0300 Subject: [PATCH 2/2] fix: remove unused variable --- src/components/molecules/NFTCard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/molecules/NFTCard.js b/src/components/molecules/NFTCard.js index 1b1fe60..527d54a 100644 --- a/src/components/molecules/NFTCard.js +++ b/src/components/molecules/NFTCard.js @@ -1,5 +1,5 @@ -import { ethers, BigNumber } from 'ethers' +import { ethers } 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'