Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Non Fungible Token Royalty (EIP2981) #3012

Merged
merged 52 commits into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
129ab6f
Add initial contracts for royalties
JulissaDantes Dec 8, 2021
3fa2485
Update interface helper/add tests
JulissaDantes Dec 9, 2021
7fb8bfd
Update 2981 tests
JulissaDantes Dec 10, 2021
e979b93
Add documentation for 2981 implementation
JulissaDantes Dec 10, 2021
5f4499a
Rename setRoyalty function
JulissaDantes Dec 13, 2021
0f814dc
Rename variables
JulissaDantes Dec 13, 2021
d8a82c8
Remove ERC165Storage inheritance
JulissaDantes Dec 13, 2021
646380b
Add different denominator logic
JulissaDantes Dec 13, 2021
9493268
Refactor royaltyInfo function
JulissaDantes Dec 13, 2021
76fa5b2
Add validations to set royalty
JulissaDantes Dec 13, 2021
f64275b
Inherit from ERC721, include burn override
JulissaDantes Dec 13, 2021
d93ede8
Add tests coverage
JulissaDantes Dec 13, 2021
6859452
Refactor tests
JulissaDantes Dec 13, 2021
f4378c5
Update contracts/token/ERC721/extensions/draft-IERC721Royalty.sol
JulissaDantes Dec 13, 2021
349fbf9
Rename variable
JulissaDantes Dec 14, 2021
44e6e6d
Remove if
JulissaDantes Dec 14, 2021
5fb5bfc
Add test case and global royalty delete
JulissaDantes Dec 14, 2021
b0f90c3
Add mixed royalties test cases
JulissaDantes Dec 14, 2021
85955fc
Avoid doing ssload twice
JulissaDantes Dec 14, 2021
9e3572d
Avoid token exclussion from global royalties tests cases
JulissaDantes Dec 15, 2021
cd33397
Update variable type
JulissaDantes Dec 15, 2021
dffd19e
Rename function and update documentation
JulissaDantes Dec 15, 2021
98bbc5d
Update contracts/token/ERC721/extensions/draft-ERC721Royalty.sol
JulissaDantes Dec 16, 2021
1fca44f
Update contracts/token/ERC721/extensions/draft-ERC721Royalty.sol
JulissaDantes Dec 16, 2021
37ccc42
Update contracts/token/ERC721/extensions/draft-ERC721Royalty.sol
JulissaDantes Dec 16, 2021
5e4d4a4
Reorder tests and rename variables
JulissaDantes Dec 16, 2021
b37621c
Remove .only
JulissaDantes Dec 16, 2021
79e01be
Rename files
JulissaDantes Dec 16, 2021
fb6facf
Add royalty implementation without token inheritance, Add ERC1155 roy…
JulissaDantes Dec 16, 2021
3d826f8
Remove double supportinterface test
JulissaDantes Dec 16, 2021
7180b20
Add the supportInterface override on the royalty base contract
JulissaDantes Dec 16, 2021
8cf5939
Add Royalty tests behavior
JulissaDantes Dec 16, 2021
1ce08da
Update ERC1155 royalty test file
JulissaDantes Dec 16, 2021
7ab210f
cleanup ERC165 override
Amxx Dec 20, 2021
c2c11bb
Update burn implementation for ERC1155
JulissaDantes Dec 20, 2021
55aa14f
Add warning detail
JulissaDantes Dec 20, 2021
2845801
Add warning details
JulissaDantes Dec 21, 2021
90feaf4
Update changelog after latest changes
JulissaDantes Dec 22, 2021
da0e9bc
whitespace
frangio Jan 6, 2022
2a848df
rename deleteRoyalty -> deleteDefaultRoyalty
frangio Jan 6, 2022
40214df
whitespace
frangio Jan 6, 2022
bf10a4f
remove slither.db.json
frangio Jan 6, 2022
a927dc7
improve docs for ERC2981
frangio Jan 6, 2022
1f7eeae
remove ERC1155Royalty, not safe to reset royalties if supply goes to …
frangio Jan 6, 2022
e2e4a56
improve ERC721Royalty docs
frangio Jan 6, 2022
e3c0f4c
Merge branch 'master' into EIP2981
frangio Jan 6, 2022
fb239db
add ERC721Royalty to ERC721 docs
frangio Jan 6, 2022
a00c10d
improve docs and reason strings
frangio Jan 6, 2022
3035b32
reorder functions more naturally
frangio Jan 6, 2022
ff12eb3
wording
frangio Jan 6, 2022
e8d141c
lint
frangio Jan 6, 2022
877a8a1
simplify docs for ERC721Royalty
frangio Jan 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

* `ERC2891`: add a new extension of `ERC721` to handle royalty information.([#3012](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3012))
* `GovernorTimelockControl`: improve the `state()` function to have it reflect cases where a proposal has been canceled directly on the timelock. ([#2977](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2977))
* `Math`: add a `abs(int256)` method that returns the unsigned absolute value of a signed value. ([#2984](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2984))
* Preset contracts are now deprecated in favor of [Contracts Wizard](https://wizard.openzeppelin.com). ([#2986](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2986))
Expand Down
4 changes: 2 additions & 2 deletions contracts/interfaces/IERC2981.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ interface IERC2981 is IERC165 {
* @param tokenId - the NFT asset queried for royalty information
* @param salePrice - 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 `salePrice`
* @return royaltyFraction - the royalty payment amount for `salePrice`
*/
function royaltyInfo(uint256 tokenId, uint256 salePrice)
external
view
returns (address receiver, uint256 royaltyAmount);
returns (address receiver, uint256 royaltyFraction);
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
}
6 changes: 6 additions & 0 deletions contracts/interfaces/draft-IERC2981.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.0 (token/ERC721/extensions/draft-ERC721Royalty.sol)

pragma solidity ^0.8.0;

import "../token/ERC721/extensions/draft-IERC721Royalty.sol";
29 changes: 29 additions & 0 deletions contracts/mocks/ERC721RoyaltyMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../token/ERC721/extensions/draft-ERC721Royalty.sol";

contract ERC721RoyaltyMock is ERC721Royalty {
constructor(string memory name, string memory symbol) ERC721(name, symbol) {}

function setTokenRoyalty(
uint256 tokenId,
address recipient,
uint256 fraction
) public {
_setTokenRoyalty(tokenId, recipient, fraction);
}

function setGlobalRoyalty(address recipient, uint256 fraction) public {
_setGlobalRoyalty(recipient, fraction);
}

function mint(address to, uint256 tokenId) public {
_mint(to, tokenId);
}

function burn(uint256 tokenId) public {
_burn(tokenId);
}
}
127 changes: 127 additions & 0 deletions contracts/token/ERC721/extensions/draft-ERC721Royalty.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.0 (token/ERC721/extensions/draft-ERC721Royalty.sol)

pragma solidity ^0.8.0;

import "../ERC721.sol";
import "./draft-IERC721Royalty.sol";
import "../../../utils/introspection/ERC165.sol";

/**
* @dev Implementation of the ERC721 Royalty extension allowing royalty information to be stored and retrieved, as defined in
* https://eips.ethereum.org/EIPS/eip-2981[EIP-2981].
*
* Adds the {_setTokenRoyalty} methods to set the token royalty information, and {_setGlobalRoyalty} method to set a global
* royalty information.
*
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
* _Available since v4.5._
*/
abstract contract ERC721Royalty is IERC721Royalty, ERC721 {
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
struct RoyaltyInfo {
address receiver;
uint256 royaltyFraction;
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
}

RoyaltyInfo private _globalRoyaltyInfo;
mapping(uint256 => RoyaltyInfo) private _tokenRoyaltyInfo;

/**
* @dev Sets tokens royalties
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
*
* Requirements:
* - `tokenId` must be already mined.
* - `receiver` cannot be the zero address.
* - `fraction` must indicate the percentage fraction using two decimals.
*/
function _setTokenRoyalty(
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
uint256 tokenId,
address receiver,
uint256 fraction
) internal virtual {
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
require(fraction > 0, "ERC2981: Royalty percentage is too low");
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
require(fraction <= _feeDenominator(), "ERC2981: Royalty percentage will exceed salePrice");
require(receiver != address(0), "ERC2981: Invalid receiver");
require(_exists(tokenId), "ERC2981: Nonexistent token");

_tokenRoyaltyInfo[tokenId] = RoyaltyInfo(receiver, fraction);
}

/**
*
* @dev Sets global royalty
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
*
* Requirements:
* - `receiver` cannot be the zero address.
* - `fraction` must indicate the percentage fraction. Needs to be set appropriately
* according to the _feeDenominator granularity.
*/
function _setGlobalRoyalty(address receiver, uint256 fraction) internal virtual {
require(fraction > 0, "ERC2981: Royalty percentage is too low");
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
require(fraction <= _feeDenominator(), "ERC2981: Royalty percentage will exceed salePrice");
require(receiver != address(0), "ERC2981: Invalid receiver");

_globalRoyaltyInfo = RoyaltyInfo(receiver, fraction);
}

/**
* @dev See {IERC721Royalty-royaltyInfo}
*/
function royaltyInfo(uint256 _tokenId, uint256 _salePrice) external view override returns (address, uint256) {
RoyaltyInfo memory royalty;

if (_tokenRoyaltyInfo[_tokenId].receiver != address(0)) {
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
royalty = _tokenRoyaltyInfo[_tokenId];
} else {
royalty = _globalRoyaltyInfo;
}

uint256 royaltyAmount = (_salePrice * royalty.royaltyFraction) / _feeDenominator();

return (royalty.receiver, royaltyAmount);
}

/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, IERC165) returns (bool) {
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
return interfaceId == type(IERC721Royalty).interfaceId || super.supportsInterface(interfaceId);
}

/**
* @dev Returns the percentage granularity being used. The default denominator is 10000
* but it can be customized by an override.
*/
function _feeDenominator() internal pure virtual returns (uint256) {
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
return 10000;
}

/**
* @dev Removes `tokenId` royalty information.
* The royalty information is cleared when the token is burned.
*
* Requirements:
*
* - `tokenId` royalty information must exist.
*
*/
function _deleteTokenRoyalty(uint256 tokenId) internal virtual {
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
if (_tokenRoyaltyInfo[tokenId].royaltyFraction != 0) {
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
delete _tokenRoyaltyInfo[tokenId];
}
}

/**
* @dev Destroys `tokenId`.
* The royalty information is cleared when the token is burned.
*
* Requirements:
*
* - `tokenId` must exist.
*
* Emits a {Transfer} event.
*/
function _burn(uint256 tokenId) internal virtual override {
super._burn(tokenId);
_deleteTokenRoyalty(tokenId);
}
}
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
29 changes: 29 additions & 0 deletions contracts/token/ERC721/extensions/draft-IERC721Royalty.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.0 (token/ERC721/extensions/draft-ERC721Royalty.sol)

pragma solidity ^0.8.0;

import "../../../interfaces/IERC165.sol";

/**
* @dev Interface for the NFT Royalty Standard.
*
* A standardized way to retrieve royalty payment information for non-fungible tokens (NFTs) to enable universal
* support for royalty payments across all NFT marketplaces and ecosystem participants.
* _Available since v4.5._
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
*/
interface IERC721Royalty is IERC165 {
/**
* @dev Called with the sale price to determine how much royalty
* is owed and to whom.
*
* Requirements:
* - `_tokenId` must be already mined, and have its royalty info set
* - `_salePrice` cannot be the zero.
*
*/
function royaltyInfo(uint256 _tokenId, uint256 _salePrice)
external
view
returns (address receiver, uint256 royaltyFraction);
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved
}
145 changes: 145 additions & 0 deletions test/token/ERC721/extensions/ERC721Royalty.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const { ZERO_ADDRESS } = constants;

const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior');

const ERC721RoyaltyMock = artifacts.require('ERC721RoyaltyMock');

contract('ERC721Royalty', function (accounts) {
const [ account1 ] = accounts;
const tokenId1 = new BN('1');
const tokenId2 = new BN('2');
const salePrice = new BN('1000');
const royaltyFraction = new BN('10');

beforeEach(async function () {
this.token = await ERC721RoyaltyMock.new('My Token', 'TKN');

await this.token.mint(account1, tokenId1);
await this.token.mint(account1, tokenId2);
});

shouldSupportInterfaces(['ERC2981']);

describe('global royalty', function () {
beforeEach(async function () {
await this.token.setGlobalRoyalty(account1, royaltyFraction);
});

it('updates royalty amount', async function () {
const newPercentage = new BN('25');
let royalty = new BN((salePrice * royaltyFraction) / 10000);
// Initial royalty check
const initInfo = await this.token.royaltyInfo(tokenId1, salePrice);

expect(initInfo[0]).to.be.equal(account1);
expect(initInfo[1]).to.be.bignumber.equal(royalty);
JulissaDantes marked this conversation as resolved.
Show resolved Hide resolved

// Updated royalty check
await this.token.setGlobalRoyalty(account1, newPercentage);
royalty = new BN((salePrice * newPercentage) / 10000);
const newInfo = await this.token.royaltyInfo(tokenId1, salePrice);

expect(newInfo[0]).to.be.equal(account1);
expect(newInfo[1]).to.be.bignumber.equal(royalty);
});

it('holds same royalty value for different tokens', async function () {
const newPercentage = new BN('20');
await this.token.setGlobalRoyalty(account1, newPercentage);

const token1Info = await this.token.royaltyInfo(tokenId1, salePrice);
const token2Info = await this.token.royaltyInfo(tokenId2, salePrice);

expect(token1Info[1]).to.be.bignumber.equal(token2Info[1]);
});

it('reverts if invalid parameters', async function () {
await expectRevert(
this.token.setGlobalRoyalty(ZERO_ADDRESS, royaltyFraction),
'ERC2981: Invalid receiver',
);

await expectRevert(
this.token.setGlobalRoyalty(account1, new BN('0')),
'ERC2981: Royalty percentage is too low',
);

await expectRevert(
this.token.setTokenRoyalty(tokenId1, account1, new BN('11000')),
'ERC2981: Royalty percentage will exceed salePrice',
);
});
});

describe('token based royalty', function () {
beforeEach(async function () {
await this.token.setTokenRoyalty(tokenId1, account1, royaltyFraction);
});

it('updates royalty amount', async function () {
const newPercentage = new BN('25');
let royalty = new BN((salePrice * royaltyFraction) / 10000);
// Initial royalty check
const initInfo = await this.token.royaltyInfo(tokenId1, salePrice);

expect(initInfo[0]).to.be.equal(account1);
expect(initInfo[1]).to.be.bignumber.equal(royalty);

// Updated royalty check
await this.token.setTokenRoyalty(tokenId1, account1, newPercentage);
royalty = new BN((salePrice * newPercentage) / 10000);
const newInfo = await this.token.royaltyInfo(tokenId1, salePrice);

expect(newInfo[0]).to.be.equal(account1);
expect(newInfo[1]).to.be.bignumber.equal(royalty);
});

it('holds different values for different tokens', async function () {
const newPercentage = new BN('20');
await this.token.setTokenRoyalty(tokenId2, account1, newPercentage);

const token1Info = await this.token.royaltyInfo(tokenId1, salePrice);
const token2Info = await this.token.royaltyInfo(tokenId2, salePrice);

// must be different even at the same SalePrice
expect(token1Info[1]).to.not.be.equal(token2Info.royaltyFraction);
});

it('reverts if invalid parameters', async function () {
await expectRevert(
this.token.setTokenRoyalty(tokenId1, ZERO_ADDRESS, royaltyFraction),
'ERC2981: Invalid receiver',
);

await expectRevert(
this.token.setTokenRoyalty(tokenId1, account1, new BN('0')),
'ERC2981: Royalty percentage is too low',
);

await expectRevert(
this.token.setTokenRoyalty(tokenId1, account1, new BN('11000')),
'ERC2981: Royalty percentage will exceed salePrice',
);

await expectRevert(
this.token.setTokenRoyalty(new BN('787'), account1, new BN('100')),
'ERC2981: Nonexistent token',
);
});

it('removes royalty information after burn', async function () {
await this.token.burn(tokenId1);
const tokenInfo = await this.token.royaltyInfo(tokenId1, salePrice);

expect(tokenInfo[0]).to.be.equal(ZERO_ADDRESS);
expect(tokenInfo[1]).to.be.bignumber.equal(new BN('0'));

await expectRevert(
this.token.setTokenRoyalty(tokenId1, account1, new BN('100')),
'ERC2981: Nonexistent token',
);
});
});
});
3 changes: 3 additions & 0 deletions test/utils/introspection/SupportsInterface.behavior.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ const INTERFACES = {
'proposalEta(uint256)',
'queue(address[],uint256[],bytes[],bytes32)',
],
ERC2981: [
'royaltyInfo(uint256,uint256)',
],
};

const INTERFACE_IDS = {};
Expand Down