Skip to content

Commit

Permalink
Implement Non Fungible Token Royalty (EIP2981) (#3012)
Browse files Browse the repository at this point in the history
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
  • Loading branch information
3 people authored Jan 6, 2022
1 parent 1e815f3 commit a65c03b
Show file tree
Hide file tree
Showing 11 changed files with 413 additions and 8 deletions.
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 implementation of the royalty standard, and the respective extensions for `ERC721` and `ERC1155`. ([#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
14 changes: 8 additions & 6 deletions contracts/interfaces/IERC2981.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ pragma solidity ^0.8.0;
import "./IERC165.sol";

/**
* @dev Interface for the NFT Royalty Standard
* @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._
*/
interface IERC2981 is IERC165 {
/**
* @dev 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 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`
* @dev Returns how much royalty is owed and to whom, based on a sale price that may be denominated in any unit of
* exchange. The royalty amount is denominated and should be payed in that same unit of exchange.
*/
function royaltyInfo(uint256 tokenId, uint256 salePrice)
external
Expand Down
33 changes: 33 additions & 0 deletions contracts/mocks/ERC721RoyaltyMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

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

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

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

function setDefaultRoyalty(address recipient, uint96 fraction) public {
_setDefaultRoyalty(recipient, fraction);
}

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

function burn(uint256 tokenId) public {
_burn(tokenId);
}

function deleteDefaultRoyalty() public {
_deleteDefaultRoyalty();
}
}
3 changes: 3 additions & 0 deletions contracts/token/ERC721/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Additionally there are a few of other extensions:

* {ERC721URIStorage}: A more flexible but more expensive way of storing metadata.
* {ERC721Votes}: Support for voting and vote delegation.
* {ERC721Royalty}: A way to signal royalty information following ERC2981.
* {ERC721Pausable}: A primitive to pause contract operation.
* {ERC721Burnable}: A way for token holders to burn their own tokens.
Expand Down Expand Up @@ -53,6 +54,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel

{{ERC721Votes}}

{{ERC721Royalty}}

== Presets

These contracts are preconfigured combinations of the above features. They can be used through inheritance or as models to copy and paste their source code.
Expand Down
38 changes: 38 additions & 0 deletions contracts/token/ERC721/extensions/ERC721Royalty.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.0 (token/ERC721/extensions/ERC721Royalty.sol)

pragma solidity ^0.8.0;

import "../ERC721.sol";
import "../../common/ERC2981.sol";
import "../../../utils/introspection/ERC165.sol";

/**
* @dev Extension of ERC721 with the ERC2981 NFT Royalty Standard, a standardized way to retrieve royalty payment
* information.
*
* Royalty information can be specified globally for all token ids via {_setDefaultRoyalty}, and/or individually for
* specific token ids via {_setTokenRoyalty}. The latter takes precedence over the first.
*
* IMPORTANT: ERC-2981 only specifies a way to signal royalty information and does not enforce its payment. See
* https://eips.ethereum.org/EIPS/eip-2981#optional-royalty-payments[Rationale] in the EIP. Marketplaces are expected to
* voluntarily pay royalties together with sales, but note that this standard is not yet widely supported.
*
* _Available since v4.5._
*/
abstract contract ERC721Royalty is ERC2981, ERC721 {
/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, ERC2981) returns (bool) {
return super.supportsInterface(interfaceId);
}

/**
* @dev See {ERC721-_burn}. This override additionally clears the royalty information for the token.
*/
function _burn(uint256 tokenId) internal virtual override {
super._burn(tokenId);
_resetTokenRoyalty(tokenId);
}
}
112 changes: 112 additions & 0 deletions contracts/token/common/ERC2981.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.0 (token/common/ERC2981.sol)

pragma solidity ^0.8.0;

import "../../interfaces/IERC2981.sol";
import "../../utils/introspection/ERC165.sol";

/**
* @dev Implementation of the NFT Royalty Standard, a standardized way to retrieve royalty payment information.
*
* Royalty information can be specified globally for all token ids via {_setDefaultRoyalty}, and/or individually for
* specific token ids via {_setTokenRoyalty}. The latter takes precedence over the first.
*
* Royalty is specified as a fraction of sale price. {_feeDenominator} is overridable but defaults to 10000, meaning the
* fee is specified in basis points by default.
*
* IMPORTANT: ERC-2981 only specifies a way to signal royalty information and does not enforce its payment. See
* https://eips.ethereum.org/EIPS/eip-2981#optional-royalty-payments[Rationale] in the EIP. Marketplaces are expected to
* voluntarily pay royalties together with sales, but note that this standard is not yet widely supported.
*
* _Available since v4.5._
*/
abstract contract ERC2981 is IERC2981, ERC165 {
struct RoyaltyInfo {
address receiver;
uint96 royaltyFraction;
}

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

/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) {
return interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
}

/**
* @inheritdoc IERC2981
*/
function royaltyInfo(uint256 _tokenId, uint256 _salePrice) external view override returns (address, uint256) {
RoyaltyInfo memory royalty = _tokenRoyaltyInfo[_tokenId];

if (royalty.receiver == address(0)) {
royalty = _defaultRoyaltyInfo;
}

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

return (royalty.receiver, royaltyAmount);
}

/**
* @dev The denominator with which to interpret the fee set in {_setTokenRoyalty} and {_setDefaultRoyalty} as a
* fraction of the sale price. Defaults to 10000 so fees are expressed in basis points, but may be customized by an
* override.
*/
function _feeDenominator() internal pure virtual returns (uint96) {
return 10000;
}

/**
* @dev Sets the royalty information that all ids in this contract will default to.
*
* Requirements:
*
* - `receiver` cannot be the zero address.
* - `feeNumerator` cannot be greater than the fee denominator.
*/
function _setDefaultRoyalty(address receiver, uint96 feeNumerator) internal virtual {
require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice");
require(receiver != address(0), "ERC2981: invalid receiver");

_defaultRoyaltyInfo = RoyaltyInfo(receiver, feeNumerator);
}

/**
* @dev Removes default royalty information.
*/
function _deleteDefaultRoyalty() internal virtual {
delete _defaultRoyaltyInfo;
}

/**
* @dev Sets the royalty information for a specific token id, overriding the global default.
*
* Requirements:
*
* - `tokenId` must be already minted.
* - `receiver` cannot be the zero address.
* - `feeNumerator` cannot be greater than the fee denominator.
*/
function _setTokenRoyalty(
uint256 tokenId,
address receiver,
uint96 feeNumerator
) internal virtual {
require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice");
require(receiver != address(0), "ERC2981: Invalid parameters");

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

/**
* @dev Resets royalty information for the token id back to the global default.
*/
function _resetTokenRoyalty(uint256 tokenId) internal virtual {
delete _tokenRoyaltyInfo[tokenId];
}
}
10 changes: 10 additions & 0 deletions contracts/token/common/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
= Common (Tokens)

Functionality that is common to multiple token standards.

* {ERC2981}: NFT Royalties compatible with both ERC721 and ERC1155.
** For ERC721 consider {ERC721Royalty} which clears the royalty information from storage on burn.
== Contracts

{{ERC2981}}
7 changes: 5 additions & 2 deletions scripts/gen-nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ const files = proc.execFileSync(
console.log('.API');

function getPageTitle (directory) {
if (directory === 'metatx') {
switch (directory) {
case 'metatx':
return 'Meta Transactions';
} else {
case 'common':
return 'Common (Tokens)';
default:
return startCase(directory);
}
}
Expand Down
40 changes: 40 additions & 0 deletions test/token/ERC721/extensions/ERC721Royalty.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const { BN, constants } = require('@openzeppelin/test-helpers');
const ERC721RoyaltyMock = artifacts.require('ERC721RoyaltyMock');
const { ZERO_ADDRESS } = constants;

const { shouldBehaveLikeERC2981 } = require('../../common/ERC2981.behavior');

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

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

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

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

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'));
});
});
shouldBehaveLikeERC2981();
});
Loading

0 comments on commit a65c03b

Please sign in to comment.