Skip to content

Commit 1e77b69

Browse files
authored
Merge pull request #147 from solidstate-network/ERC2981
ERC2981
2 parents 4472093 + b0355c3 commit 1e77b69

File tree

12 files changed

+391
-0
lines changed

12 files changed

+391
-0
lines changed

abi/ERC2981.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
[
2+
{
3+
"inputs": [
4+
{
5+
"internalType": "uint256",
6+
"name": "tokenId",
7+
"type": "uint256"
8+
},
9+
{
10+
"internalType": "uint256",
11+
"name": "salePrice",
12+
"type": "uint256"
13+
}
14+
],
15+
"name": "royaltyInfo",
16+
"outputs": [
17+
{
18+
"internalType": "address",
19+
"name": "",
20+
"type": "address"
21+
},
22+
{
23+
"internalType": "uint256",
24+
"name": "",
25+
"type": "uint256"
26+
}
27+
],
28+
"stateMutability": "view",
29+
"type": "function"
30+
},
31+
{
32+
"inputs": [
33+
{
34+
"internalType": "bytes4",
35+
"name": "interfaceId",
36+
"type": "bytes4"
37+
}
38+
],
39+
"name": "supportsInterface",
40+
"outputs": [
41+
{
42+
"internalType": "bool",
43+
"name": "",
44+
"type": "bool"
45+
}
46+
],
47+
"stateMutability": "view",
48+
"type": "function"
49+
}
50+
]

abi/IERC2981.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
[
2+
{
3+
"inputs": [
4+
{
5+
"internalType": "uint256",
6+
"name": "tokenId",
7+
"type": "uint256"
8+
},
9+
{
10+
"internalType": "uint256",
11+
"name": "salePrice",
12+
"type": "uint256"
13+
}
14+
],
15+
"name": "royaltyInfo",
16+
"outputs": [
17+
{
18+
"internalType": "address",
19+
"name": "receiever",
20+
"type": "address"
21+
},
22+
{
23+
"internalType": "uint256",
24+
"name": "royaltyAmount",
25+
"type": "uint256"
26+
}
27+
],
28+
"stateMutability": "view",
29+
"type": "function"
30+
},
31+
{
32+
"inputs": [
33+
{
34+
"internalType": "bytes4",
35+
"name": "interfaceId",
36+
"type": "bytes4"
37+
}
38+
],
39+
"name": "supportsInterface",
40+
"outputs": [
41+
{
42+
"internalType": "bool",
43+
"name": "",
44+
"type": "bool"
45+
}
46+
],
47+
"stateMutability": "view",
48+
"type": "function"
49+
}
50+
]

contracts/interfaces/IERC2981.sol

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.8;
4+
5+
import { IERC165 } from './IERC165.sol';
6+
import { IERC2981Internal } from './IERC2981Internal.sol';
7+
8+
/**
9+
* @title ERC2981 interface
10+
* @dev see https://eips.ethereum.org/EIPS/eip-2981
11+
*/
12+
interface IERC2981 is IERC2981Internal, IERC165 {
13+
/**
14+
* @notice called with the sale price to determine how much royalty is owed and to whom
15+
* @param tokenId the ERC721 or ERC1155 token id to query for royalty information
16+
* @param salePrice the sale price of the given asset
17+
* @return receiever rightful recipient of royalty
18+
* @return royaltyAmount amount of royalty owed
19+
*/
20+
function royaltyInfo(uint256 tokenId, uint256 salePrice)
21+
external
22+
view
23+
returns (address receiever, uint256 royaltyAmount);
24+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.8;
4+
5+
/**
6+
* @title ERC2981 interface
7+
*/
8+
interface IERC2981Internal {
9+
10+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.8;
4+
5+
import { IERC2981 } from '../../../interfaces/IERC2981.sol';
6+
7+
import { ERC2981Storage } from './ERC2981Storage.sol';
8+
import { ERC2981Internal } from './ERC2981Internal.sol';
9+
10+
/**
11+
* @title ERC2981 implementation
12+
*/
13+
abstract contract ERC2981 is IERC2981, ERC2981Internal {
14+
/**
15+
* @notice inheritdoc IERC2981
16+
*/
17+
function royaltyInfo(uint256 tokenId, uint256 salePrice)
18+
external
19+
view
20+
returns (address, uint256)
21+
{
22+
return _royaltyInfo(tokenId, salePrice);
23+
}
24+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.8;
4+
5+
import { ERC2981Storage } from './ERC2981Storage.sol';
6+
import { IERC2981Internal } from '../../../interfaces/IERC2981Internal.sol';
7+
8+
/**
9+
* @title ERC2981 internal functions
10+
*/
11+
abstract contract ERC2981Internal is IERC2981Internal {
12+
/**
13+
* @notice calculate how much royalty is owed and to whom
14+
* @dev royalty must be paid in addition to, rather than deducted from, salePrice
15+
* @param tokenId the ERC721 or ERC1155 token id to query for royalty information
16+
* @param salePrice the sale price of the given asset
17+
* @return royaltyReceiver rightful recipient of royalty
18+
* @return royalty amount of royalty owed
19+
*/
20+
function _royaltyInfo(uint256 tokenId, uint256 salePrice)
21+
internal
22+
view
23+
virtual
24+
returns (address royaltyReceiver, uint256 royalty)
25+
{
26+
uint256 royaltyBPS = _getRoyaltyBPS(tokenId);
27+
28+
// intermediate multiplication overflow is theoretically possible here, but
29+
// not an issue in practice because of practical constraints of salePrice
30+
return (_getRoyaltyReceiver(tokenId), (royaltyBPS * salePrice) / 10000);
31+
}
32+
33+
/**
34+
* @notice query the royalty rate (denominated in basis points) for given token id
35+
* @dev implementation supports per-token-id values as well as a global default
36+
* @param tokenId token whose royalty rate to query
37+
* @return royaltyBPS royalty rate
38+
*/
39+
function _getRoyaltyBPS(uint256 tokenId)
40+
internal
41+
view
42+
virtual
43+
returns (uint16 royaltyBPS)
44+
{
45+
ERC2981Storage.Layout storage l = ERC2981Storage.layout();
46+
royaltyBPS = l.royaltiesBPS[tokenId];
47+
48+
if (royaltyBPS == 0) {
49+
royaltyBPS = l.defaultRoyaltyBPS;
50+
}
51+
}
52+
53+
/**
54+
* @notice query the royalty receiver for given token id
55+
* @dev implementation supports per-token-id values as well as a global default
56+
* @param tokenId token whose royalty receiver to query
57+
* @return royaltyReceiver royalty receiver
58+
*/
59+
function _getRoyaltyReceiver(uint256 tokenId)
60+
internal
61+
view
62+
virtual
63+
returns (address royaltyReceiver)
64+
{
65+
ERC2981Storage.Layout storage l = ERC2981Storage.layout();
66+
royaltyReceiver = l.royaltyReceivers[tokenId];
67+
68+
if (royaltyReceiver == address(0)) {
69+
royaltyReceiver = l.defaultRoyaltyReceiver;
70+
}
71+
}
72+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.8;
4+
5+
import { ERC2981, IERC2981 } from './ERC2981.sol';
6+
import { ERC2981Storage } from './ERC2981Storage.sol';
7+
8+
import { IERC165, ERC165, ERC165Storage } from '../../../introspection/ERC165.sol';
9+
10+
contract ERC2981Mock is ERC2981, ERC165 {
11+
using ERC165Storage for ERC165Storage.Layout;
12+
13+
constructor(
14+
uint16 defaultRoyaltyBPS,
15+
uint16[] memory royaltiesBPS,
16+
address defaultRoyaltyReceiver
17+
) {
18+
{
19+
ERC2981Storage.Layout storage l = ERC2981Storage.layout();
20+
l.defaultRoyaltyBPS = defaultRoyaltyBPS;
21+
l.defaultRoyaltyReceiver = defaultRoyaltyReceiver;
22+
23+
for (uint8 i = 0; i < royaltiesBPS.length; i++) {
24+
l.royaltiesBPS[i] = royaltiesBPS[i];
25+
}
26+
}
27+
28+
{
29+
ERC165Storage.Layout storage l = ERC165Storage.layout();
30+
l.setSupportedInterface(type(IERC165).interfaceId, true);
31+
l.setSupportedInterface(type(IERC2981).interfaceId, true);
32+
}
33+
}
34+
35+
function setRoyalty(uint16 defaultRoyaltyBPS) external {
36+
ERC2981Storage.layout().defaultRoyaltyBPS = defaultRoyaltyBPS;
37+
}
38+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.8;
4+
5+
library ERC2981Storage {
6+
struct Layout {
7+
// token id -> royalty (denominated in basis points)
8+
mapping(uint256 => uint16) royaltiesBPS;
9+
uint16 defaultRoyaltyBPS;
10+
// token id -> receiver address
11+
mapping(uint256 => address) royaltyReceivers;
12+
address defaultRoyaltyReceiver;
13+
}
14+
15+
bytes32 internal constant STORAGE_SLOT =
16+
keccak256('solidstate.contracts.storage.ERC2981');
17+
18+
function layout() internal pure returns (Layout storage l) {
19+
bytes32 slot = STORAGE_SLOT;
20+
assembly {
21+
l.slot := slot
22+
}
23+
}
24+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describeBehaviorOfERC165 } from '../../introspection';
2+
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
3+
import { describeFilter } from '@solidstate/library';
4+
import { ERC2981Mock } from '@solidstate/typechain-types';
5+
import { expect } from 'chai';
6+
import { BigNumber } from 'ethers';
7+
import { ethers } from 'hardhat';
8+
9+
export function describeBehaviorOfERC2981(
10+
deploy: () => Promise<ERC2981Mock>,
11+
skips?: string[],
12+
) {
13+
const describe = describeFilter(skips);
14+
15+
describe('::ERC2981', function () {
16+
let tokenIdOne = BigNumber.from(1);
17+
let tokenIdTwo = BigNumber.from(2);
18+
let tokenIdThree = BigNumber.from(3);
19+
20+
let receiver: SignerWithAddress;
21+
let instance: ERC2981Mock;
22+
23+
before(async function () {
24+
receiver = (await ethers.getSigners())[1];
25+
instance = await deploy();
26+
});
27+
28+
describeBehaviorOfERC165(
29+
deploy,
30+
{
31+
interfaceIds: ['0x2a55205a'],
32+
},
33+
skips,
34+
);
35+
36+
describe('#royaltyInfo()', () => {
37+
it('returns 0 if salePrice is 0', async function () {
38+
const [, royaltyAmount] = await instance.royaltyInfo(
39+
0,
40+
BigNumber.from(0),
41+
);
42+
43+
expect(royaltyAmount).to.equal(BigNumber.from(0));
44+
});
45+
46+
it('returns receiver address', async function () {
47+
const [recipient] = await instance.royaltyInfo(0, BigNumber.from(0));
48+
expect(recipient).to.equal(await receiver.getAddress());
49+
});
50+
51+
it('calculates royalty using global if local does not exist', async function () {
52+
let [, royaltyAmount] = await instance.royaltyInfo(0, 10000);
53+
expect(royaltyAmount).to.equal(BigNumber.from(10000));
54+
});
55+
56+
it('calculates royalty using local', async function () {
57+
let [, royaltyAmount] = await instance.royaltyInfo(tokenIdOne, 10000);
58+
expect(royaltyAmount).to.equal(BigNumber.from(100));
59+
60+
[, royaltyAmount] = await instance.royaltyInfo(tokenIdTwo, 10000);
61+
expect(royaltyAmount).to.equal(BigNumber.from(1000));
62+
});
63+
});
64+
});
65+
}

spec/token/common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './ERC2981.behavior';

0 commit comments

Comments
 (0)