From 4bacb0b3daf27e574f84934720d01b397d2dd95b Mon Sep 17 00:00:00 2001 From: Yosif Hamed Date: Wed, 30 Oct 2024 19:16:22 +0200 Subject: [PATCH 1/2] Update legends nft contract --- contracts/LegendsNft.sol | 151 +++++++++++++++++++++------------- hardhat.config.ts | 2 +- test/LegendsNft/LegendsNft.ts | 28 +++++-- tsconfig.json | 2 +- 4 files changed, 117 insertions(+), 66 deletions(-) diff --git a/contracts/LegendsNft.sol b/contracts/LegendsNft.sol index 1f9702868..83f2c8c9a 100644 --- a/contracts/LegendsNft.sol +++ b/contracts/LegendsNft.sol @@ -1,66 +1,101 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; -import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/utils/Strings.sol"; +import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol'; +import '@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol'; +import '@openzeppelin/contracts/access/Ownable.sol'; +import '@openzeppelin/contracts/utils/Strings.sol'; using Strings for address; - contract LegendsNFT is IERC721Metadata, ERC721Enumerable, Ownable { - event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); - event MetadataUpdate(uint256 _tokenId); - event PickedCharacter(uint indexed heroType); - - string baseURI; - - constructor() ERC721("Ambire Legends", "AML") Ownable() {} - - function supportsInterface(bytes4 interfaceId) public view override(ERC721Enumerable, IERC165) returns(bool) { - return interfaceId == 0x49064906 || super.supportsInterface(interfaceId); - } - - function mint(uint heroType) public { - // single mint allowed - // using address for front-end simplification - _mint(msg.sender, uint256(uint160(msg.sender))); - emit PickedCharacter(heroType); - } - - function tokenURI(uint256 tokenId) public view override(ERC721, IERC721Metadata) returns (string memory) { - _requireMinted(tokenId); - return string(abi.encodePacked(baseURI, address(uint160(tokenId)).toHexString())); - } - - function setBaseUri(string calldata _baseURI) public onlyOwner { - baseURI = _baseURI; - } - - // this is used only for - function updateOpenSeaInfo(uint from, uint to) public { - emit BatchMetadataUpdate(from, to); - } - - // Soulbound - function approve(address, uint256) public pure override(ERC721, IERC721) { - revert("Soulbound: cannot approve token transfer"); - } - - function setApprovalForAll(address, bool) public pure override(ERC721, IERC721) { - revert("Soulbound: cannot set approval for all"); - } - - function transferFrom(address, address, uint256) public pure override(ERC721, IERC721) { - revert("Soulbound: cannot transfer nft"); - } - - function safeTransferFrom(address, address, uint256) public pure override(ERC721, IERC721) { - revert("Soulbound: cannot transfer nft"); - } - - function safeTransferFrom(address, address, uint256, bytes memory ) public pure override(ERC721, IERC721) { - revert("Soulbound: cannot transfer nft"); - } + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); + event MetadataUpdate(uint256 _tokenId); + event PickedCharacter(uint indexed heroType); + bool allowTransfers = false; + string baseURI; + + constructor() ERC721('Ambire Legends', 'AML') Ownable() {} + + function supportsInterface( + bytes4 interfaceId + ) public view override(ERC721Enumerable, IERC165) returns (bool) { + return interfaceId == 0x49064906 || super.supportsInterface(interfaceId); + } + + function mint(uint heroType) public { + // single mint allowed + // using address for front-end simplification + _mint(msg.sender, uint256(uint160(msg.sender))); + emit PickedCharacter(heroType); + // this line triggers opensea metadata updaate + emit BatchMetadataUpdate(0, type(uint256).max); + } + + function tokenURI( + uint256 tokenId + ) public view override(ERC721, IERC721Metadata) returns (string memory) { + _requireMinted(tokenId); + return string(abi.encodePacked(baseURI, address(uint160(tokenId)).toHexString())); + } + + function setBaseUri(string calldata _baseURI) public onlyOwner { + baseURI = _baseURI; + } + + // this is used only for + function updateOpenSeaInfo(uint from, uint to) public { + emit BatchMetadataUpdate(from, to); + } + + function setAllowTransfer(bool value) public onlyOwner { + allowTransfers = value; + } + + function burn(uint256 tokenId) public { + require(allowTransfers, 'Soulbound: cannot burn'); + require( + _msgSender() == _ownerOf(tokenId) || _msgSender() == owner(), + 'You cannot burn this NFT.' + ); + _burn(tokenId); + } + + function approve(address recipient, uint256 tokenId) public override(ERC721, IERC721) { + require(allowTransfers, 'Soulbound: cannot approve token transfer'); + _approve(recipient, tokenId); + } + + function setApprovalForAll(address recipient, bool isApproved) public override(ERC721, IERC721) { + require(allowTransfers, 'Soulbound: cannot set approval for all'); + _setApprovalForAll(_msgSender(), recipient, isApproved); + } + + function transferFrom( + address from, + address to, + uint256 tokenId + ) public override(ERC721, IERC721) { + require(allowTransfers, 'Soulbound: cannot transfer nft'); + _transfer(from, to, tokenId); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public override(ERC721, IERC721) { + require(allowTransfers, 'Soulbound: cannot transfer nft'); + _safeTransfer(from, to, tokenId, ''); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public override(ERC721, IERC721) { + require(allowTransfers, 'Soulbound: cannot transfer nft'); + _safeTransfer(from, to, tokenId, data); + } } diff --git a/hardhat.config.ts b/hardhat.config.ts index 0eb08ea55..a9052e537 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -43,6 +43,6 @@ const config: HardhatUserConfig = { } ] } -} +} as any export default config diff --git a/test/LegendsNft/LegendsNft.ts b/test/LegendsNft/LegendsNft.ts index 72d48491c..71ed0c04c 100644 --- a/test/LegendsNft/LegendsNft.ts +++ b/test/LegendsNft/LegendsNft.ts @@ -1,3 +1,5 @@ +import '@nomicfoundation/hardhat-chai-matchers' + import { expect } from 'chai' import { hexlify } from 'ethers' import { ethers } from 'hardhat' @@ -32,7 +34,7 @@ describe('Legends nft', () => { }) it('set base uri not owner', async () => { - await expect(legendsNftContract.connect(signer2).setBaseUri('')).to.be.rejectedWith( + await expect(legendsNftContract.connect(signer2).setBaseUri('')).to.be.revertedWith( 'Ownable: caller is not the owner' ) }) @@ -43,10 +45,10 @@ describe('Legends nft', () => { await expect( legendsNftContract.transferFrom(signer.address, signer2.address, BigInt(signer.address)) - ).to.be.rejectedWith('Soulbound: cannot transfer nft') + ).to.be.revertedWith('Soulbound: cannot transfer nft') await expect( legendsNftContract.safeTransferFrom(signer.address, signer2.address, BigInt(signer.address)) - ).to.be.rejectedWith('Soulbound: cannot transfer nft') + ).to.be.revertedWith('Soulbound: cannot transfer nft') await expect( legendsNftContract['safeTransferFrom(address,address,uint256,bytes)']( signer.address, @@ -54,14 +56,28 @@ describe('Legends nft', () => { BigInt(signer.address), ethers.toUtf8Bytes('asd') ) - ).to.be.rejectedWith('Soulbound: cannot transfer nft') + ).to.be.revertedWith('Soulbound: cannot transfer nft') await expect( legendsNftContract.approve(signer2.address, BigInt(signer.address)) - ).to.be.rejectedWith('Soulbound: cannot approve token transfer') + ).to.be.revertedWith('Soulbound: cannot approve token transfer') - await expect(legendsNftContract.setApprovalForAll(signer2.address, true)).to.be.rejectedWith( + await expect(legendsNftContract.setApprovalForAll(signer2.address, true)).to.be.revertedWith( 'Soulbound: cannot set approval for all' ) + + await legendsNftContract.setAllowTransfer(true) + await expect( + legendsNftContract.connect(signer2).burn(BigInt(signer.address)) + ).to.be.revertedWith('You cannot burn this NFT.') + + await legendsNftContract.burn(BigInt(signer.address)) + await legendsNftContract.burn(BigInt(signer2.address)) + await expect(legendsNftContract.ownerOf(BigInt(signer.address))).to.be.revertedWith( + 'ERC721: invalid token ID' + ) + await expect(legendsNftContract.ownerOf(BigInt(signer2.address))).to.be.revertedWith( + 'ERC721: invalid token ID' + ) }) it('opensea update', async () => { const supportsInterface721 = await legendsNftContract.supportsInterface(hexlify('0x80ac58cd')) diff --git a/tsconfig.json b/tsconfig.json index 2be76e22b..8a1823887 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "noEmitOnError": true, "noEmitHelpers": true, "lib": ["ESNext", "DOM"], - "types": ["node", "jest"], + "types": ["node", "jest", "mocha"], "target": "ESNext", "moduleResolution": "node", "allowSyntheticDefaultImports": true, From 193e2554f46a0bc10b56611961570005c3936e3e Mon Sep 17 00:00:00 2001 From: Yosif Hamed Date: Tue, 5 Nov 2024 15:41:16 +0200 Subject: [PATCH 2/2] add mint after burn in test --- test/LegendsNft/LegendsNft.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/LegendsNft/LegendsNft.ts b/test/LegendsNft/LegendsNft.ts index 71ed0c04c..77daa8921 100644 --- a/test/LegendsNft/LegendsNft.ts +++ b/test/LegendsNft/LegendsNft.ts @@ -78,6 +78,8 @@ describe('Legends nft', () => { await expect(legendsNftContract.ownerOf(BigInt(signer2.address))).to.be.revertedWith( 'ERC721: invalid token ID' ) + await legendsNftContract.mint(2) + expect(await legendsNftContract.ownerOf(BigInt(signer.address))).eq(signer.address) }) it('opensea update', async () => { const supportsInterface721 = await legendsNftContract.supportsInterface(hexlify('0x80ac58cd'))