diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e230f90..36609de 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,14 +5,14 @@ on: branches: - main - dev - - 'feature/*' - - 'features/*' + - "feature/*" + - "features/*" pull_request: branches: - main - dev - - 'feature/*' - - 'features/*' + - "feature/*" + - "features/*" env: FOUNDRY_PROFILE: ci @@ -23,24 +23,21 @@ jobs: fail-fast: true name: Foundry project - runs-on: [self-hosted, dockerize] + runs-on: ubuntu-latest steps: - - id: 'gh-app' - name: 'Get Token' - uses: 'tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a' #v1.7.0 - with: - app_id: ${{ secrets.GH_APP_ID }} - private_key: ${{ secrets.GH_PRIVATE_KEY }} - - uses: actions/checkout@v4.1.1 with: submodules: recursive - token: ${{ steps.gh-app.outputs.token }} - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: version: nightly + + - name: Install dependencies + run: | + forge install + id: install - name: Run Forge build run: | diff --git a/remappings.txt b/remappings.txt index 82ca5a0..7779b1e 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,4 @@ forge-std/=lib/forge-std/src/ ds-test/=lib/forge-std/lib/ds-test/src/ -@openzeppelin/=./node_modules/@openzeppelin/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts diff --git a/src/ERC1155Common.sol b/src/ERC1155Common.sol index 52e95be..d91ba8a 100644 --- a/src/ERC1155Common.sol +++ b/src/ERC1155Common.sol @@ -1,72 +1,137 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT +// Compatible with OpenZeppelin Contracts ^5.0.0 +pragma solidity ^0.8.20; +import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol"; +import { AccessControlEnumerable } from "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; import { ERC1155 } from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; + +import { ERC1155Burnable } from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol"; +import { ERC1155Pausable } from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Pausable.sol"; import { ERC1155Supply } from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol"; -import { AccessControlEnumerable } from "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IERC1155Common } from "./interfaces/IERC1155Common.sol"; -contract ERC1155Common is AccessControlEnumerable, ERC1155, ERC1155Supply, IERC1155Common { +contract ERC1155Common is + ERC1155, + AccessControlEnumerable, + ERC1155Pausable, + ERC1155Burnable, + ERC1155Supply, + IERC1155Common +{ using Strings for uint256; + bytes32 public constant URI_SETTER_ROLE = keccak256("URI_SETTER_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + string private _name; string private _symbol; - constructor(string memory name_, string memory symbol_, string memory baseTokenURI, address[] memory minters) - payable - ERC1155(baseTokenURI) - { + constructor(address admin, string memory name_, string memory symbol_, string memory uri_) ERC1155(uri_) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(PAUSER_ROLE, admin); + _grantRole(MINTER_ROLE, admin); + _grantRole(URI_SETTER_ROLE, admin); + _name = name_; _symbol = symbol_; - _grantRole(DEFAULT_ADMIN_ROLE, _msgSender()); - bytes32 minterRole = MINTER_ROLE; - - uint256 length = minters.length; - for (uint256 i; i < length;) { - _grantRole(minterRole, minters[i]); - unchecked { - ++i; - } - } } - function uri(uint256 tokenId) public view override returns (string memory) { - string memory _uri = super.uri(tokenId); - return string(abi.encodePacked(_uri, tokenId.toString())); + /** + * @dev Set the URI for all token types. + * Requirements: + * - the caller must have the `URI_SETTER_ROLE`. + */ + function setURI(string memory newURI) external onlyRole(URI_SETTER_ROLE) { + _setURI(newURI); + } + + /** + * @dev Pauses all token transfers. + * Requirements: + * - the caller must have the `PAUSER_ROLE`. + */ + function pause() external onlyRole(PAUSER_ROLE) { + _pause(); } - function name() external view returns (string memory) { - return _name; + /** + * @dev Unpauses all token transfers. + * Requirements: + * - the caller must have the `PAUSER_ROLE`. + */ + function unpause() external onlyRole(PAUSER_ROLE) { + _unpause(); } - function symbol() external view returns (string memory) { - return _symbol; + /// @inheritdoc IERC1155Common + function mint(address account, uint256 id, uint256 amount, bytes calldata data) public virtual onlyRole(MINTER_ROLE) { + _mint(account, id, amount, data); + } + + /// @inheritdoc IERC1155Common + function mintBatch(address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) + public + virtual + onlyRole(MINTER_ROLE) + { + _mintBatch(to, ids, amounts, data); + } + + /** + * @dev Mint single token to multiple addresses. + * Requirements: + * - the caller must have the `MINTER_ROLE`. + */ + function bulkMint(uint256 id, address[] calldata tos, uint256[] calldata amounts, bytes[] calldata datas) + public + virtual + onlyRole(MINTER_ROLE) + { + uint256 length = tos.length; + require(length != 0 && length == amounts.length && length == datas.length, "ERC1155: invalid array lengths"); + + for (uint256 i; i < length; ++i) { + _mint(tos[i], id, amounts[i], datas[i]); + } } /** - * @dev Sets the base URI for metadata of ERC1155 tokens. - * @param uri_ The new base URI. + * @dev See {ERC1155-uri}. */ - function setURI(string calldata uri_) external onlyRole(DEFAULT_ADMIN_ROLE) { - _setURI(uri_); + function uri(uint256 tokenId) public view virtual override returns (string memory) { + string memory uri_ = super.uri(tokenId); + return string.concat(uri_, tokenId.toString()); } /// @inheritdoc IERC1155Common - function mint(address to, uint256 id, uint256 amount) external onlyRole(MINTER_ROLE) { - _mint(to, id, amount, ""); + function name() public view virtual returns (string memory) { + return _name; } /// @inheritdoc IERC1155Common - function batchMint(address to, uint256[] calldata ids, uint256[] calldata amounts) external onlyRole(MINTER_ROLE) { - _mintBatch(to, ids, amounts, ""); + function symbol() public view virtual returns (string memory) { + return _symbol; } - function supportsInterface(bytes4 interfaceId) public view override(ERC1155, AccessControlEnumerable) returns (bool) { + /** + * @dev See {ERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC1155, AccessControlEnumerable) + returns (bool) + { return interfaceId == type(IERC1155Common).interfaceId || super.supportsInterface(interfaceId); } + /** + * @dev See {ERC1155-_beforeTokenTransfer}. + */ function _beforeTokenTransfer( address operator, address from, @@ -74,7 +139,7 @@ contract ERC1155Common is AccessControlEnumerable, ERC1155, ERC1155Supply, IERC1 uint256[] memory ids, uint256[] memory amounts, bytes memory data - ) internal virtual override(ERC1155, ERC1155Supply) { + ) internal virtual override(ERC1155, ERC1155Pausable, ERC1155Supply) { super._beforeTokenTransfer(operator, from, to, ids, amounts, data); } } diff --git a/src/ERC721Common.sol b/src/ERC721Common.sol index ca99a2a..db72bc9 100644 --- a/src/ERC721Common.sol +++ b/src/ERC721Common.sol @@ -6,7 +6,7 @@ import "./interfaces/IERC721Common.sol"; import "./refs/ERC721Nonce.sol"; import "./ERC721PresetMinterPauserAutoIdCustomized.sol"; -abstract contract ERC721Common is ERC721Nonce, ERC721PresetMinterPauserAutoIdCustomized, IERC721State, IERC721Common { +contract ERC721Common is ERC721Nonce, ERC721PresetMinterPauserAutoIdCustomized, IERC721State, IERC721Common { constructor(string memory name, string memory symbol, string memory baseTokenURI) ERC721PresetMinterPauserAutoIdCustomized(name, symbol, baseTokenURI) { } diff --git a/src/interfaces/IERC1155Common.sol b/src/interfaces/IERC1155Common.sol index 36171a8..4df32ae 100644 --- a/src/interfaces/IERC1155Common.sol +++ b/src/interfaces/IERC1155Common.sol @@ -1,20 +1,51 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -interface IERC1155Common { +import { IAccessControlEnumerable } from "@openzeppelin/contracts/access/IAccessControlEnumerable.sol"; +import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +interface IERC1155Common is IAccessControlEnumerable, IERC1155 { + /// @dev Return the name of the collection. + function name() external view returns (string memory); + + /// @dev Return the symbol of the collection. + function symbol() external view returns (string memory); + /** * @dev Mints a single ERC1155 token and assigns it to the specified address. + * + * Requirements: + * - the caller must have the `MINTER_ROLE`. + * * @param to The address to which the minted token will be assigned. * @param id The ID of the token to mint. * @param amount The amount of tokens to mint. + * @param data Additional data with no specified format. */ - function mint(address to, uint256 id, uint256 amount) external; + function mint(address to, uint256 id, uint256 amount, bytes calldata data) external; /** * @dev Mints multiple ERC1155 tokens and assigns them to the specified address. + * + * Requirements: + * - the caller must have the `MINTER_ROLE`. + * * @param to The address to which the minted tokens will be assigned. * @param ids The IDs of the tokens to mint. * @param amounts The amounts of tokens to mint. + * @param data Additional data with no specified format. + */ + function mintBatch(address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external; + + /** + * @dev Mint single token to multiple addresses. + * Requirements: + * - the caller must have the `MINTER_ROLE`. + * + * @param id The ID of the token to mint. + * @param tos The addresses to which the minted tokens will be assigned. + * @param amounts The amounts of tokens to mint. + * @param datas Additional data with no specified format. */ - function batchMint(address to, uint256[] calldata ids, uint256[] calldata amounts) external; + function bulkMint(uint256 id, address[] calldata tos, uint256[] calldata amounts, bytes[] calldata datas) external; } diff --git a/src/mock/SampleERC1155.sol b/src/mock/SampleERC1155.sol index 878f611..e919819 100644 --- a/src/mock/SampleERC1155.sol +++ b/src/mock/SampleERC1155.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import "../ERC1155Common.sol"; contract SampleERC1155 is ERC1155Common { - constructor(string memory name, string memory symbol, string memory baseTokenURI, address[] memory minters) - ERC1155Common(name, symbol, baseTokenURI, minters) - {} + constructor(address admin, string memory name, string memory symbol, string memory uri) + ERC1155Common(admin, name, symbol, uri) + { } } diff --git a/src/mock/launchpad/SampleNFT1155Launchpad.sol b/src/mock/launchpad/SampleNFT1155Launchpad.sol index b070646..67fb4c7 100644 --- a/src/mock/launchpad/SampleNFT1155Launchpad.sol +++ b/src/mock/launchpad/SampleNFT1155Launchpad.sol @@ -1,17 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; -import { ERC1155 } from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; -import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; import { NFTLaunchpadCommon } from "../../launchpad/NFTLaunchpadCommon.sol"; +import { SampleERC1155, ERC1155Common } from "../SampleERC1155.sol"; -contract SampleNFT1155Launchpad is ERC1155, AccessControl, NFTLaunchpadCommon { - bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - - constructor(address admin, address minter, string memory uri_) ERC1155(uri_) { - _setupRole(DEFAULT_ADMIN_ROLE, admin); - _setupRole(MINTER_ROLE, minter); - } +contract SampleNFT1155Launchpad is SampleERC1155, NFTLaunchpadCommon { + constructor(address admin, string memory name, string memory symbol, string memory uri) + SampleERC1155(admin, name, symbol, uri) + { } /// @dev Mint NFTs for the launchpad. function mintLaunchpad(address to, uint256 quantity, bytes calldata /* extraData */ ) @@ -35,9 +31,9 @@ contract SampleNFT1155Launchpad is ERC1155, AccessControl, NFTLaunchpadCommon { public view virtual - override(ERC1155, AccessControl, NFTLaunchpadCommon) + override(ERC1155Common, NFTLaunchpadCommon) returns (bool) { - return super.supportsInterface(interfaceId); + return ERC1155Common.supportsInterface(interfaceId) || NFTLaunchpadCommon.supportsInterface(interfaceId); } } diff --git a/src/mock/launchpad/SampleNFT721Launchpad.sol b/src/mock/launchpad/SampleNFT721Launchpad.sol index 6317bee..79e77a1 100644 --- a/src/mock/launchpad/SampleNFT721Launchpad.sol +++ b/src/mock/launchpad/SampleNFT721Launchpad.sol @@ -31,6 +31,6 @@ contract SampleNFT721Launchpad is SampleERC721, NFTLaunchpadCommon { override(ERC721Common, NFTLaunchpadCommon) returns (bool) { - return super.supportsInterface(interfaceId); + return ERC721Common.supportsInterface(interfaceId) || NFTLaunchpadCommon.supportsInterface(interfaceId); } } diff --git a/test/foundry/SampleERC1155.t.sol b/test/foundry/SampleERC1155.t.sol index 6e753e9..c723c9b 100644 --- a/test/foundry/SampleERC1155.t.sol +++ b/test/foundry/SampleERC1155.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { SampleERC1155, ERC1155Common } from "../../src/mock/SampleERC1155.sol"; +import { IERC165 } from "@openzeppelin/contracts/interfaces/IERC165.sol"; import { IERC1155 } from "@openzeppelin/contracts/interfaces/IERC1155.sol"; import { IAccessControlEnumerable } from "@openzeppelin/contracts/access/IAccessControlEnumerable.sol"; import { IERC1155Common } from "src/interfaces/IERC1155Common.sol"; @@ -14,12 +15,12 @@ contract SampleERC1155Test is Test { string public constant NAME = "SampleERC1155"; string public constant SYMBOL = "NFT1155"; string public constant BASE_URI = "http://example.com/"; - address[] public minters = [address(1)]; + address admin = makeAddr("admin"); ERC1155Common internal _t; function setUp() public virtual { - _t = new SampleERC1155(NAME, SYMBOL, BASE_URI, minters); + _t = new SampleERC1155(admin, NAME, SYMBOL, BASE_URI); } function testName() public virtual { @@ -36,12 +37,12 @@ contract SampleERC1155Test is Test { } function testMint() public virtual { - vm.startPrank(address(1)); - _token().mint(address(15), 15, 15); + vm.startPrank(admin); + _token().mint(address(15), 15, 15, ""); assertEq(_token().totalSupply(15), 15); assertEq(_token().balanceOf(address(15), 15), 15); - _token().mint(address(20), 15, 15); + _token().mint(address(20), 15, 15, ""); assertEq(_token().totalSupply(15), 30); assertEq(_token().balanceOf(address(20), 15), 15); vm.stopPrank(); @@ -51,6 +52,7 @@ contract SampleERC1155Test is Test { assertEq(_token().supportsInterface(type(IERC1155).interfaceId), true); assertEq(_token().supportsInterface(type(IAccessControlEnumerable).interfaceId), true); assertEq(_token().supportsInterface(type(IERC1155Common).interfaceId), true); + assertEq(_token().supportsInterface(type(IERC165).interfaceId), true); } function _token() internal view virtual returns (ERC1155Common) { diff --git a/test/foundry/SampleNFT1155Launchpad.t.sol b/test/foundry/SampleNFT1155Launchpad.t.sol new file mode 100644 index 0000000..2731fa1 --- /dev/null +++ b/test/foundry/SampleNFT1155Launchpad.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { SampleNFT1155Launchpad, SampleERC1155 } from "../../src/mock/launchpad/SampleNFT1155Launchpad.sol"; +import { INFTLaunchpad } from "src/interfaces/launchpad/INFTLaunchpad.sol"; +import { IERC1155Common, IAccessControlEnumerable, IERC1155 } from "src/interfaces/IERC1155Common.sol"; + +contract SampleERC1155LaunchpadTest is Test { + using Strings for uint256; + + address admin = makeAddr("admin"); + string public constant NAME = "SampleERC721"; + string public constant SYMBOL = "NFT"; + string public constant BASE_URI = "http://example.com/"; + + SampleNFT1155Launchpad internal _t; + + function setUp() public virtual { + _t = new SampleNFT1155Launchpad(admin, NAME, SYMBOL, BASE_URI); + } + + function testSupportsInterface() public { + assertEq(_token().supportsInterface(type(INFTLaunchpad).interfaceId), true); + assertEq(_token().supportsInterface(type(IERC1155Common).interfaceId), true); + assertEq(_token().supportsInterface(type(IAccessControlEnumerable).interfaceId), true); + assertEq(_token().supportsInterface(type(IERC1155).interfaceId), true); + } + + function _token() internal view virtual returns (SampleNFT1155Launchpad) { + return _t; + } +} diff --git a/test/foundry/SampleNFT721Launchpad.t.sol b/test/foundry/SampleNFT721Launchpad.t.sol new file mode 100644 index 0000000..1d5d0c0 --- /dev/null +++ b/test/foundry/SampleNFT721Launchpad.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { SampleNFT721Launchpad } from "../../src/mock/launchpad/SampleNFT721Launchpad.sol"; +import { INFTLaunchpad } from "src/interfaces/launchpad/INFTLaunchpad.sol"; +import { IERC721Common } from "src/interfaces/IERC721Common.sol"; + +contract SampleNFT721LaunchpadTest is Test { + using Strings for uint256; + + string public constant NAME = "SampleERC721"; + string public constant SYMBOL = "NFT"; + string public constant BASE_URI = "http://example.com/"; + + SampleNFT721Launchpad internal _t; + + function setUp() public virtual { + _t = new SampleNFT721Launchpad(NAME, SYMBOL, BASE_URI); + } + + function testSupportInterface() public { + assertEq(_token().supportsInterface(type(INFTLaunchpad).interfaceId), true); + assertEq(_token().supportsInterface(type(IERC721Common).interfaceId), true); + } + + function _token() internal view virtual returns (SampleNFT721Launchpad) { + return _t; + } +}