diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000..fa1d536 --- /dev/null +++ b/.trivyignore @@ -0,0 +1 @@ +CVE-2024-45296 \ No newline at end of file diff --git a/samples/solidity/contracts/ERC721NoData.sol b/samples/solidity/contracts/ERC721NoData.sol index 2b9a554..0fcd97a 100644 --- a/samples/solidity/contracts/ERC721NoData.sol +++ b/samples/solidity/contracts/ERC721NoData.sol @@ -9,11 +9,12 @@ import '@openzeppelin/contracts/access/Ownable.sol'; /** * Example ERC721 token with mint and burn. - * - * This contract is identical to ERC721WithData, except that there is no way to record - * extra data alongside any of the token operations. While FireFly can still index - * the transactions and balances from this type of ABI, certain features will not be - * available (such as tieing FireFly transactions, messages, and data to a token event). + * - Tokens are auto-indexed (starting from 1) + * - Only the contract owner can mint + * - Token URIs are generated using a placeholder format "firefly://token/" + * - No extra "data" argument is present on mint/burn/transfer methods, meaning that + * certain features of FireFly will not be available (such as tieing FireFly transactions, + * messages, and data to a token event) * * This is a sample only and NOT a reference implementation. */ diff --git a/samples/solidity/contracts/ERC721URI.sol b/samples/solidity/contracts/ERC721URI.sol new file mode 100644 index 0000000..872fd52 --- /dev/null +++ b/samples/solidity/contracts/ERC721URI.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.0; + +import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; +import '@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol'; +import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol'; +import '@openzeppelin/contracts/access/Ownable.sol'; + +/** + * Example ERC721 token with mint and burn. + * - Tokens are auto-indexed (starting from 1) + * - Only the contract owner can mint + * - Token URIs are set explicitly at mint time + * - No extra "data" argument is present on mint/burn/transfer methods, meaning that + * certain features of FireFly will not be available (such as tieing FireFly transactions, + * messages, and data to a token event) + * + * This is a sample only and NOT a reference implementation. + */ +contract ERC721URI is ERC721, ERC721URIStorage, ERC721Burnable, Ownable { + uint256 private _nextTokenId = 1; + + constructor( + string memory name, + string memory symbol + ) ERC721(name, symbol) Ownable(msg.sender) {} + + function safeMint(address to, string memory uri) public onlyOwner { + uint256 tokenId = _nextTokenId++; + _safeMint(to, tokenId); + _setTokenURI(tokenId, uri); + } + + // The following functions are overrides required by Solidity. + + function tokenURI( + uint256 tokenId + ) public view override(ERC721, ERC721URIStorage) returns (string memory) { + return super.tokenURI(tokenId); + } + + function supportsInterface( + bytes4 interfaceId + ) public view override(ERC721, ERC721URIStorage) returns (bool) { + return super.supportsInterface(interfaceId); + } +} diff --git a/samples/solidity/test/ERC721URI.ts b/samples/solidity/test/ERC721URI.ts new file mode 100644 index 0000000..5b78cda --- /dev/null +++ b/samples/solidity/test/ERC721URI.ts @@ -0,0 +1,268 @@ +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { ERC721URI } from '../typechain-types'; + +describe('ERC721URI - Unit Tests', async function () { + const contractName = 'testName'; + const contractSymbol = 'testSymbol'; + const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + let deployedERC721: ERC721URI; + let Factory; + + let deployerSignerA: SignerWithAddress; + let signerB: SignerWithAddress; + let signerC: SignerWithAddress; + + beforeEach(async () => { + [deployerSignerA, signerB, signerC] = await ethers.getSigners(); + Factory = await ethers.getContractFactory('ERC721URI'); + // Deploy erc721 token pool with Signer A + deployedERC721 = await Factory.connect(deployerSignerA).deploy(contractName, contractSymbol); + await deployedERC721.waitForDeployment(); + }); + + it('Create - Should create a new ERC721 instance with default state', async function () { + expect(await deployedERC721.name()).to.equal(contractName); + expect(await deployedERC721.symbol()).to.equal(contractSymbol); + }); + + it('Mint - Non-deployer cannot mint', async function () { + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + + // Signer B mint to Signer B (Not allowed) + await expect( + deployedERC721.connect(signerB).safeMint(signerB.address, 'ipfs://token1'), + ).to.be.revertedWithCustomError(deployedERC721, 'OwnableUnauthorizedAccount'); + + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(0); + }); + + it('Mint - Deployer should mint tokens to itself successfully', async function () { + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + // Signer A mint token to Signer A (Allowed) + await expect( + deployedERC721.connect(deployerSignerA).safeMint(deployerSignerA.address, 'ipfs://token1'), + ) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, deployerSignerA.address, 1) + .and.to.emit(deployedERC721, 'MetadataUpdate') + .withArgs('1'); + + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(1); + expect(await deployedERC721.tokenURI(1)).to.equal('ipfs://token1'); + }); + + it('Mint - Non-deployer of contract should not be able to mint tokens', async function () { + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(0); + // Signer B mint token to Signer B (Not allowed) + await expect( + deployedERC721.connect(signerB).safeMint(signerB.address, 'ipfs://token1'), + ).to.be.revertedWithCustomError(deployedERC721, 'OwnableUnauthorizedAccount'); + + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(0); + }); + + it('Transfer+Burn - Signer should transfer tokens to another signer, who may then burn', async function () { + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(0); + + // Signer A mint token to Signer A + await expect( + deployedERC721.connect(deployerSignerA).safeMint(deployerSignerA.address, 'ipfs://token1'), + ) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, deployerSignerA.address, 1); + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(1); + expect(await deployedERC721.tokenURI(1)).to.equal('ipfs://token1'); + + // Signer A transfer token to Signer B + await expect( + deployedERC721 + .connect(deployerSignerA) + ['safeTransferFrom(address,address,uint256)'](deployerSignerA.address, signerB.address, 1), + ) + .to.emit(deployedERC721, 'Transfer') + .withArgs(deployerSignerA.address, signerB.address, 1); + + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(1); + + // Signer B burn + await expect(deployedERC721.connect(signerB).burn(1)) + .to.emit(deployedERC721, 'Transfer') + .withArgs(signerB.address, ZERO_ADDRESS, 1); + + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(0); + }); + + it("Transfer - Approved signer should transfer tokens from approving signer's wallet", async function () { + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerC.address)).to.equal(0); + // Signer A mint token to Signer B + await expect(deployedERC721.connect(deployerSignerA).safeMint(signerB.address, 'ipfs://token1')) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, signerB.address, 1); + // Signer B approves signer A for token + deployedERC721.connect(signerB).approve(deployerSignerA.address, 1); + // Signer A transfers token from signer B to Signer C + await expect( + deployedERC721 + .connect(deployerSignerA) + ['safeTransferFrom(address,address,uint256)'](signerB.address, signerC.address, 1), + ) + .to.emit(deployedERC721, 'Transfer') + .withArgs(signerB.address, signerC.address, 1); + + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerC.address)).to.equal(1); + }); + + it("Transfer - Approved signer should not transfer unapproved token ID from approving signer's wallet", async function () { + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerC.address)).to.equal(0); + // Signer A mint to Signer B + await expect(deployedERC721.connect(deployerSignerA).safeMint(signerB.address, 'ipfs://token1')) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, signerB.address, 1); + // Signer A mint to Signer B + await expect(deployedERC721.connect(deployerSignerA).safeMint(signerB.address, 'ipfs://token1')) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, signerB.address, 2); + // Signer B approves signer A for token + deployedERC721.connect(signerB).approve(deployerSignerA.address, 2); + // Signer A transfers token from signer B to Signer C (Not Allowed) + await expect( + deployedERC721 + .connect(deployerSignerA) + ['safeTransferFrom(address,address,uint256)'](signerB.address, signerC.address, 1), + ).to.be.revertedWithCustomError(deployedERC721, 'ERC721InsufficientApproval'); + + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(2); + expect(await deployedERC721.balanceOf(signerC.address)).to.equal(0); + }); + + it('Transfer - Signer should not be able to transfer from another signer if not approved', async function () { + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerC.address)).to.equal(0); + // Mint token token to Signer B + await expect(deployedERC721.connect(deployerSignerA).safeMint(signerB.address, 'ipfs://token1')) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, signerB.address, 1); + // Mint token to Signer C + await expect(deployedERC721.connect(deployerSignerA).safeMint(signerC.address, 'ipfs://token1')) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, signerC.address, 2); + // Signer B attempts to transfer token from Signer C to Signer B (Not allowed) + await expect( + deployedERC721 + .connect(signerB) + ['safeTransferFrom(address,address,uint256)'](signerC.address, signerB.address, 1), + ).to.be.reverted; + // Signer C attempts to transfer token from Signer B to Signer C (Not allowed) + await expect( + deployedERC721 + .connect(signerC) + ['safeTransferFrom(address,address,uint256)'](signerB.address, signerC.address, 2), + ).to.be.reverted; + + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(1); + expect(await deployedERC721.balanceOf(signerC.address)).to.equal(1); + }); + + it('Burn - Signer should burn their own tokens successfully', async function () { + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + // Mint tokens to Signer A + await expect( + deployedERC721.connect(deployerSignerA).safeMint(deployerSignerA.address, 'ipfs://token1'), + ) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, deployerSignerA.address, 1); + await expect( + deployedERC721.connect(deployerSignerA).safeMint(deployerSignerA.address, 'ipfs://token1'), + ) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, deployerSignerA.address, 2); + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(2); + // Signer A burns token + await expect(deployedERC721.connect(deployerSignerA).burn(1)) + .to.emit(deployedERC721, 'Transfer') + .withArgs(deployerSignerA.address, ZERO_ADDRESS, 1); + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(1); + // Signer A burns token + await expect(deployedERC721.connect(deployerSignerA).burn(2)) + .to.emit(deployedERC721, 'Transfer') + .withArgs(deployerSignerA.address, ZERO_ADDRESS, 2); + + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + }); + + it("Burn - Signer should not burn another signer's tokens", async function () { + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerC.address)).to.equal(0); + // Signer A mints token to itself + await expect( + deployedERC721.connect(deployerSignerA).safeMint(deployerSignerA.address, 'ipfs://token1'), + ) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, deployerSignerA.address, 1); + // Signer A mints token to Signer B + await expect(deployedERC721.connect(deployerSignerA).safeMint(signerB.address, 'ipfs://token1')) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, signerB.address, 2); + // Signer A mints token to Signer C + await expect(deployedERC721.connect(deployerSignerA).safeMint(signerC.address, 'ipfs://token1')) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, signerC.address, 3); + // Signer B attempts to burn token from Signer A wallet (not allowed) + await expect(deployedERC721.connect(signerB).burn(1)).to.be.revertedWithCustomError( + deployedERC721, + 'ERC721InsufficientApproval', + ); + // Signer C attempts to burn token from Signer B wallet (not allowed) + await expect(deployedERC721.connect(signerC).burn(2)).to.be.revertedWithCustomError( + deployedERC721, + 'ERC721InsufficientApproval', + ); + + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(1); + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(1); + expect(await deployedERC721.balanceOf(signerC.address)).to.equal(1); + }); + + it('URI - Minted token URIs should be set', async function () { + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(0); + expect(await deployedERC721.balanceOf(signerC.address)).to.equal(0); + // Signer A mints token to itself + await expect( + deployedERC721.connect(deployerSignerA).safeMint(deployerSignerA.address, 'ipfs://token1'), + ) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, deployerSignerA.address, 1); + // Signer A mints token to Signer B + await expect(deployedERC721.connect(deployerSignerA).safeMint(signerB.address, 'ipfs://token2')) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, signerB.address, 2); + // Signer A mints token to Signer C + await expect(deployedERC721.connect(deployerSignerA).safeMint(signerC.address, 'ipfs://token3')) + .to.emit(deployedERC721, 'Transfer') + .withArgs(ZERO_ADDRESS, signerC.address, 3); + + expect(await deployedERC721.tokenURI(1)).to.equal('ipfs://token1'); + expect(await deployedERC721.tokenURI(2)).to.equal('ipfs://token2'); + expect(await deployedERC721.tokenURI(3)).to.equal('ipfs://token3'); + + expect(await deployedERC721.balanceOf(deployerSignerA.address)).to.equal(1); + expect(await deployedERC721.balanceOf(signerB.address)).to.equal(1); + expect(await deployedERC721.balanceOf(signerC.address)).to.equal(1); + }); +});