Repository: dievardump/EIP2981-implementation Branch: main Commit: efb71d618472 Files: 19 Total size: 22.8 KB Directory structure: gitextract_6dcip9tb/ ├── .gitignore ├── .prettierrc ├── .solhint.json ├── README.md ├── contracts/ │ ├── ERC2981Base.sol │ ├── ERC2981ContractWideRoyalties.sol │ ├── ERC2981PerTokenRoyalties.sol │ ├── IERC2981Royalties.sol │ └── mocks/ │ ├── ERC1155WithRoyalties.sol │ ├── ERC721WithContractWideRoyalties.sol │ └── ERC721WithRoyalties.sol ├── deploy/ │ ├── 00_deploy_erc721.js │ ├── 01_deploy_erc1155.js │ └── 02_deploy_erc721_contract_wide.js ├── hardhat.config.js ├── package.json └── test/ ├── 00_royalties_721.js ├── 01_royalties_1155.js └── 02_royalties_721_contract_wide.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules #Hardhat files cache artifacts .env* !.env.SAMPLE ================================================ FILE: .prettierrc ================================================ { "trailingComma": "all", "tabWidth": 4, "useTabs": false, "semi": true, "singleQuote": true } ================================================ FILE: .solhint.json ================================================ { "extends": "solhint:default" } ================================================ FILE: README.md ================================================ # EIP-2981 Example ## Resume Simple example of implementation of EIP-2981. In all implementations, royalties are expected to be from 0 to 10000 which allows to define royalties with 2 decimals. ### Contract wide royalties The contract `ERC2981ContractWideRoyalties.sol` implements ERC2981 with every token having the same royalties recipient and amount. `./contracts/mocks/ERC721WithContractWideRoyalties.sol` is an example of ERC721 that would work on a contract-wide royalty base. ### Per Token royalties The contract `ERC2981PerTokenRoyalties.sol` implements ERC2981 with every token having different royalties The contracts - `./contracts/mocks/ERC721WithRoyalties.sol` - `./contracts/mocks/ERC1155WithRoyalties.sol` show how to extend ERC2981 to set royalties at minting time. ## Installation Clone this repository using your favorite tool (i.e: `git clone git@github.com:dievardump/EIP2981-implementation.git`) In the directory of installation: `npm install` ## Compilation `npm run compile` ## Tests `npm run test` ================================================ FILE: contracts/ERC2981Base.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import '@openzeppelin/contracts/utils/introspection/ERC165.sol'; import './IERC2981Royalties.sol'; /// @dev This is a contract used to add ERC2981 support to ERC721 and 1155 abstract contract ERC2981Base is ERC165, IERC2981Royalties { struct RoyaltyInfo { address recipient; uint24 amount; } /// @inheritdoc ERC165 function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { return interfaceId == type(IERC2981Royalties).interfaceId || super.supportsInterface(interfaceId); } } ================================================ FILE: contracts/ERC2981ContractWideRoyalties.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import '@openzeppelin/contracts/utils/introspection/ERC165.sol'; import './ERC2981Base.sol'; /// @dev This is a contract used to add ERC2981 support to ERC721 and 1155 /// @dev This implementation has the same royalties for each and every tokens abstract contract ERC2981ContractWideRoyalties is ERC2981Base { RoyaltyInfo private _royalties; /// @dev Sets token royalties /// @param recipient recipient of the royalties /// @param value percentage (using 2 decimals - 10000 = 100, 0 = 0) function _setRoyalties(address recipient, uint256 value) internal { require(value <= 10000, 'ERC2981Royalties: Too high'); _royalties = RoyaltyInfo(recipient, uint24(value)); } /// @inheritdoc IERC2981Royalties function royaltyInfo(uint256, uint256 value) external view override returns (address receiver, uint256 royaltyAmount) { RoyaltyInfo memory royalties = _royalties; receiver = royalties.recipient; royaltyAmount = (value * royalties.amount) / 10000; } } ================================================ FILE: contracts/ERC2981PerTokenRoyalties.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import '@openzeppelin/contracts/utils/introspection/ERC165.sol'; import './ERC2981Base.sol'; /// @dev This is a contract used to add ERC2981 support to ERC721 and 1155 abstract contract ERC2981PerTokenRoyalties is ERC2981Base { mapping(uint256 => RoyaltyInfo) internal _royalties; /// @dev Sets token royalties /// @param tokenId the token id fir which we register the royalties /// @param recipient recipient of the royalties /// @param value percentage (using 2 decimals - 10000 = 100, 0 = 0) function _setTokenRoyalty( uint256 tokenId, address recipient, uint256 value ) internal { require(value <= 10000, 'ERC2981Royalties: Too high'); _royalties[tokenId] = RoyaltyInfo(recipient, uint24(value)); } /// @inheritdoc IERC2981Royalties function royaltyInfo(uint256 tokenId, uint256 value) external view override returns (address receiver, uint256 royaltyAmount) { RoyaltyInfo memory royalties = _royalties[tokenId]; receiver = royalties.recipient; royaltyAmount = (value * royalties.amount) / 10000; } } ================================================ FILE: contracts/IERC2981Royalties.sol ================================================ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; /// @title IERC2981Royalties /// @dev Interface for the ERC2981 - Token Royalty standard interface IERC2981Royalties { /// @notice Called with the sale price to determine how much royalty // is owed and to whom. /// @param _tokenId - the NFT asset queried for royalty information /// @param _value - the sale price of the NFT asset specified by _tokenId /// @return _receiver - address of who should be sent the royalty payment /// @return _royaltyAmount - the royalty payment amount for value sale price function royaltyInfo(uint256 _tokenId, uint256 _value) external view returns (address _receiver, uint256 _royaltyAmount); } ================================================ FILE: contracts/mocks/ERC1155WithRoyalties.sol ================================================ //SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import '@openzeppelin/contracts/token/ERC1155/ERC1155.sol'; import '../ERC2981PerTokenRoyalties.sol'; /// @title Example of ERC1155 contract with ERC2981 /// @author Simon Fremaux (@dievardump) /// @notice This is a mock, mint and mintBatch are not protected. Please do not use as-is in production contract ERC1155WithRoyalties is ERC1155, ERC2981PerTokenRoyalties { constructor(string memory uri_) ERC1155(uri_) {} /// @inheritdoc ERC165 function supportsInterface(bytes4 interfaceId) public view virtual override(ERC1155, ERC2981Base) returns (bool) { return super.supportsInterface(interfaceId); } /// @notice Mint amount token of type `id` to `to` /// @param to the recipient of the token /// @param id id of the token type to mint /// @param amount amount of the token type to mint /// @param royaltyRecipient the recipient for royalties (if royaltyValue > 0) /// @param royaltyValue the royalties asked for (EIP2981) function mint( address to, uint256 id, uint256 amount, address royaltyRecipient, uint256 royaltyValue ) external { _mint(to, id, amount, ''); if (royaltyValue > 0) { _setTokenRoyalty(id, royaltyRecipient, royaltyValue); } } /// @notice Mint several tokens at once /// @param to the recipient of the token /// @param ids array of ids of the token types to mint /// @param amounts array of amount to mint for each token type /// @param royaltyRecipients an array of recipients for royalties (if royaltyValues[i] > 0) /// @param royaltyValues an array of royalties asked for (EIP2981) function mintBatch( address to, uint256[] memory ids, uint256[] memory amounts, address[] memory royaltyRecipients, uint256[] memory royaltyValues ) external { require( ids.length == royaltyRecipients.length && ids.length == royaltyValues.length, 'ERC1155: Arrays length mismatch' ); _mintBatch(to, ids, amounts, ''); for (uint256 i; i < ids.length; i++) { if (royaltyValues[i] > 0) { _setTokenRoyalty( ids[i], royaltyRecipients[i], royaltyValues[i] ); } } } } ================================================ FILE: contracts/mocks/ERC721WithContractWideRoyalties.sol ================================================ //SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; import '../ERC2981ContractWideRoyalties.sol'; /// @title Example of ERC721 contract with ERC2981 /// @author Simon Fremaux (@dievardump) /// @notice This is a mock, mint and mintBatch are not protected. Please do not use as-is in production contract ERC721WithContractWideRoyalties is ERC721, ERC2981ContractWideRoyalties { uint256 nextTokenId; constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {} /// @inheritdoc ERC165 function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, ERC2981Base) returns (bool) { return super.supportsInterface(interfaceId); } /// @notice Allows to set the royalties on the contract /// @dev This function in a real contract should be protected with a onlyOwner (or equivalent) modifier /// @param recipient the royalties recipient /// @param value royalties value (between 0 and 10000) function setRoyalties(address recipient, uint256 value) public { _setRoyalties(recipient, value); } /// @notice Mint one token to `to` /// @param to the recipient of the token function mint(address to) external { uint256 tokenId = nextTokenId; _safeMint(to, tokenId, ''); nextTokenId = tokenId + 1; } /// @notice Mint several tokens at once /// @param recipients an array of recipients for each token function mintBatch(address[] memory recipients) external { uint256 tokenId = nextTokenId; for (uint256 i; i < recipients.length; i++) { _safeMint(recipients[i], tokenId, ''); tokenId++; } nextTokenId = tokenId; } } ================================================ FILE: contracts/mocks/ERC721WithRoyalties.sol ================================================ //SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; import '../ERC2981PerTokenRoyalties.sol'; /// @title Example of ERC721 contract with ERC2981 /// @author Simon Fremaux (@dievardump) /// @notice This is a mock, mint and mintBatch are not protected. Please do not use as-is in production contract ERC721WithRoyalties is ERC721, ERC2981PerTokenRoyalties { uint256 nextTokenId; constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {} /// @inheritdoc ERC165 function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, ERC2981Base) returns (bool) { return super.supportsInterface(interfaceId); } /// @notice Mint one token to `to` /// @param to the recipient of the token /// @param royaltyRecipient the recipient for royalties (if royaltyValue > 0) /// @param royaltyValue the royalties asked for (EIP2981) function mint( address to, address royaltyRecipient, uint256 royaltyValue ) external { uint256 tokenId = nextTokenId; _safeMint(to, tokenId, ''); if (royaltyValue > 0) { _setTokenRoyalty(tokenId, royaltyRecipient, royaltyValue); } nextTokenId = tokenId + 1; } /// @notice Mint several tokens at once /// @param recipients an array of recipients for each token /// @param royaltyRecipients an array of recipients for royalties (if royaltyValues[i] > 0) /// @param royaltyValues an array of royalties asked for (EIP2981) function mintBatch( address[] memory recipients, address[] memory royaltyRecipients, uint256[] memory royaltyValues ) external { uint256 tokenId = nextTokenId; require( recipients.length == royaltyRecipients.length && recipients.length == royaltyValues.length, 'ERC721: Arrays length mismatch' ); for (uint256 i; i < recipients.length; i++) { _safeMint(recipients[i], tokenId, ''); if (royaltyValues[i] > 0) { _setTokenRoyalty( tokenId, royaltyRecipients[i], royaltyValues[i] ); } tokenId++; } nextTokenId = tokenId; } } ================================================ FILE: deploy/00_deploy_erc721.js ================================================ // deploy/00_deploy_my_contract.js module.exports = async ({ getNamedAccounts, deployments }) => { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); await deploy('ERC721WithRoyalties', { from: deployer, args: ['ERC721WithRoyalties', '2981'], log: true, }); }; module.exports.tags = ['ERC721WithRoyalties']; ================================================ FILE: deploy/01_deploy_erc1155.js ================================================ // deploy/00_deploy_my_contract.js module.exports = async ({ getNamedAccounts, deployments }) => { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); await deploy('ERC1155WithRoyalties', { from: deployer, args: ['ipfs://baseURI'], log: true, }); }; module.exports.tags = ['ERC1155WithRoyalties']; ================================================ FILE: deploy/02_deploy_erc721_contract_wide.js ================================================ // deploy/00_deploy_my_contract.js module.exports = async ({ getNamedAccounts, deployments }) => { const { deploy } = deployments; const { deployer } = await getNamedAccounts(); await deploy('ERC721WithContractWideRoyalties', { from: deployer, args: ['ERC721WithContractWideRoyalties', '2981'], log: true, }); }; module.exports.tags = ['ERC721WithContractWideRoyalties']; ================================================ FILE: hardhat.config.js ================================================ require('@nomiclabs/hardhat-ethers'); require('@nomiclabs/hardhat-waffle'); require('hardhat-deploy'); require('hardhat-deploy-ethers'); require('hardhat-tracer'); require('@nomiclabs/hardhat-etherscan'); require("hardhat-gas-reporter"); /** * @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: { version: '0.8.0', settings: { optimizer: { enabled: true, runs: 200, }, }, }, namedAccounts: { deployer: { default: 0, // here this will by default take the first account as deployer }, }, }; ================================================ FILE: package.json ================================================ { "name": "eip2981-example", "version": "0.0.1", "description": "EIP2981 implementation example", "main": "index.js", "scripts": { "compile": "npx hardhat compile", "test": "npx hardhat test" }, "author": "simon fremaux (dievarump)", "license": "ISC", "devDependencies": { "@nomiclabs/hardhat-ethers": "^2.0.2", "@nomiclabs/hardhat-etherscan": "^2.1.3", "@nomiclabs/hardhat-waffle": "^2.0.1", "@openzeppelin/contracts": "^4.1.0", "chai": "^4.3.4", "ethereum-waffle": "^3.4.0", "ethers": "^5.3.1", "hardhat": "^2.4.0", "hardhat-deploy": "^0.8.8", "hardhat-deploy-ethers": "*", "hardhat-gas-reporter": "1.0.4", "hardhat-tracer": "*" } } ================================================ FILE: test/00_royalties_721.js ================================================ const { expect } = require('chai'); const { deployments, ethers } = require('hardhat'); const _INTERFACE_ID_ERC165 = '0x01ffc9a7'; const _INTERFACE_ID_ROYALTIES_EIP2981 = '0x2a55205a'; const _INTERFACE_ID_ERC721 = '0x80ac58cd'; describe('ERC721WithRoyalties', () => { let ERC721WithRoyalties; let deployer; let royaltiesRecipient; const ADDRESS_ZERO = ethers.constants.AddressZero; beforeEach(async () => { [deployer, randomAccount, royaltiesRecipient] = await ethers.getSigners(); await deployments.fixture(); ERC721WithRoyalties = await deployments.get('ERC721WithRoyalties'); erc721WithRoyalties = await ethers.getContractAt( 'ERC721WithRoyalties', ERC721WithRoyalties.address, deployer, ); }); describe('Royalties', async () => { it('has all the right interfaces', async function () { expect( await erc721WithRoyalties.supportsInterface( _INTERFACE_ID_ERC165, ), 'Error Royalties 165', ).to.be.true; expect( await erc721WithRoyalties.supportsInterface( _INTERFACE_ID_ROYALTIES_EIP2981, ), 'Error Royalties 2981', ).to.be.true; expect( await erc721WithRoyalties.supportsInterface( _INTERFACE_ID_ERC721, ), 'Error Royalties 721', ).to.be.true; }); it('throws if royalties more than 100%', async function () { const tx = erc721WithRoyalties.mint( deployer.address, royaltiesRecipient.address, 10001, // 100.01% ); await expect(tx).to.be.revertedWith('ERC2981Royalties: Too high'); }); it('has the right royalties for tokenId', async function () { await erc721WithRoyalties.mint( deployer.address, royaltiesRecipient.address, 250, // 2.50% ); const info = await erc721WithRoyalties.royaltyInfo(0, 10000); expect(info[1].toNumber()).to.be.equal(250); expect(info[0]).to.be.equal(royaltiesRecipient.address); }); it('can set address(0) as royalties recipient', async function () { // 0.01% royalties await erc721WithRoyalties.mint(deployer.address, ADDRESS_ZERO, 1); const info = await erc721WithRoyalties.royaltyInfo(0, 10000); expect(info[1].toNumber()).to.be.equal(1); expect(info[0]).to.be.equal(ADDRESS_ZERO); }); it('has no royalties if not set', async function () { await erc721WithRoyalties.mint( deployer.address, royaltiesRecipient.address, 0, ); const info = await erc721WithRoyalties.royaltyInfo(0, 100); expect(info[1].toNumber()).to.be.equal(0); expect(info[0]).to.be.equal(ADDRESS_ZERO); }); }); }); ================================================ FILE: test/01_royalties_1155.js ================================================ const { expect } = require('chai'); const { deployments, ethers } = require('hardhat'); describe('ERC1155WithRoyalties', () => { let ERC1155WithRoyalties; let deployer; let randomAccount; let royaltiesRecipient; const ADDRESS_ZERO = ethers.constants.AddressZero; beforeEach(async () => { [deployer, randomAccount, royaltiesRecipient] = await ethers.getSigners(); await deployments.fixture(); ERC1155WithRoyalties = await deployments.get('ERC1155WithRoyalties'); erc1155WithRoyalties = await ethers.getContractAt( 'ERC1155WithRoyalties', ERC1155WithRoyalties.address, deployer, ); }); describe('Royalties', async () => { it('throws if royalties more than 100%', async function () { const tx = erc1155WithRoyalties.mint( deployer.address, 0, 10, royaltiesRecipient.address, 10001, // 100.01% ); await expect(tx).to.be.revertedWith('ERC2981Royalties: Too high'); }); it('has the right royalties for tokenId', async function () { await erc1155WithRoyalties.mint( deployer.address, 0, 10, royaltiesRecipient.address, 1000, // 10% ); const info = await erc1155WithRoyalties.royaltyInfo(0, 100); expect(info[1].toNumber()).to.be.equal(10); expect(info[0]).to.be.equal(royaltiesRecipient.address); }); it('can set address(0) as royalties recipient', async function () { await erc1155WithRoyalties.mint( deployer.address, 0, 10, ADDRESS_ZERO, 1000, ); const info = await erc1155WithRoyalties.royaltyInfo(0, 100); expect(info[1].toNumber()).to.be.equal(10); expect(info[0]).to.be.equal(ADDRESS_ZERO); }); it('has no royalties if not set', async function () { await erc1155WithRoyalties.mint( deployer.address, 0, 10, royaltiesRecipient.address, 0, ); const info = await erc1155WithRoyalties.royaltyInfo(0, 100); expect(info[1].toNumber()).to.be.equal(0); expect(info[0]).to.be.equal(ADDRESS_ZERO); }); it('each token has the right royalties when minting batch', async function () { const ids = [0, 1, 2, 3]; const amounts = [10, 10, 10, 10]; const royaltyRecipients = [ randomAccount.address, deployer.address, ADDRESS_ZERO, deployer.address, ]; const royaltyValues = [1000, 800, 5000, 0]; await erc1155WithRoyalties.mintBatch( deployer.address, ids, amounts, royaltyRecipients, royaltyValues, ); for (const [index, id] of ids.entries()) { const info = await erc1155WithRoyalties.royaltyInfo(id, 100); expect(info[1].toNumber()).to.be.equal( royaltyValues[index] / 100, ); if (info[1].toNumber() !== 0) { expect(info[0]).to.be.equal(royaltyRecipients[index]); } } }); }); }); ================================================ FILE: test/02_royalties_721_contract_wide.js ================================================ const { expect } = require('chai'); const { deployments, ethers } = require('hardhat'); describe('ERC721WithContractWideRoyalties', () => { let ERC721WithContractWideRoyalties; let deployer; let royaltiesRecipient; const ADDRESS_ZERO = ethers.constants.AddressZero; beforeEach(async () => { [deployer, randomAccount, royaltiesRecipient] = await ethers.getSigners(); await deployments.fixture(); ERC721WithContractWideRoyalties = await deployments.get( 'ERC721WithContractWideRoyalties', ); erc721WithRoyalties = await ethers.getContractAt( 'ERC721WithContractWideRoyalties', ERC721WithContractWideRoyalties.address, deployer, ); }); describe('Contract wide Royalties', async () => { it('has no royalties if not set', async function () { await erc721WithRoyalties.mint(deployer.address); const info = await erc721WithRoyalties.royaltyInfo(0, 100); expect(info[1].toNumber()).to.be.equal(0); expect(info[0]).to.be.equal(ADDRESS_ZERO); }); it('throws if royalties more than 100%', async function () { const tx = erc721WithRoyalties.setRoyalties( royaltiesRecipient.address, 10001, ); await expect(tx).to.be.revertedWith('ERC2981Royalties: Too high'); }); it('has the right royalties for tokenId', async function () { await erc721WithRoyalties.setRoyalties( royaltiesRecipient.address, 250, ); await erc721WithRoyalties.mint(deployer.address); const info = await erc721WithRoyalties.royaltyInfo(0, 10000); expect(info[1].toNumber()).to.be.equal(250); expect(info[0]).to.be.equal(royaltiesRecipient.address); }); it('can set address(0) as royalties recipient', async function () { await erc721WithRoyalties.setRoyalties(ADDRESS_ZERO, 5000); await erc721WithRoyalties.mint(deployer.address); const info = await erc721WithRoyalties.royaltyInfo(0, 10000); expect(info[1].toNumber()).to.be.equal(5000); expect(info[0]).to.be.equal(ADDRESS_ZERO); }); }); });