Skip to content

Commit bb651ad

Browse files
Add support for RoyaltyRegistry / RoyaltyEngine (#418)
* royalty registry support * prettier * lint * public view function for royalty engine address * fix * Mock royalty engine * royalty tests for direct-listings * prettier * fix tests * test royalty engine for english-auctions * test royalty engine for offers * docs * v3.6.1-0 --------- Co-authored-by: nkrishang <[email protected]>
1 parent 9f4d313 commit bb651ad

17 files changed

+1704
-116
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.0;
4+
5+
/// @author: manifold.xyz
6+
7+
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
8+
9+
/**
10+
* @dev Lookup engine interface
11+
*/
12+
interface IRoyaltyEngineV1 is IERC165 {
13+
/**
14+
* Get the royalty for a given token (address, id) and value amount. Does not cache the bps/amounts. Caches the spec for a given token address
15+
*
16+
* @param tokenAddress - The address of the token
17+
* @param tokenId - The id of the token
18+
* @param value - The value you wish to get the royalty of
19+
*
20+
* returns Two arrays of equal length, royalty recipients and the corresponding amount each recipient should get
21+
*/
22+
function getRoyalty(
23+
address tokenAddress,
24+
uint256 tokenId,
25+
uint256 value
26+
) external returns (address payable[] memory recipients, uint256[] memory amounts);
27+
28+
/**
29+
* View only version of getRoyalty
30+
*
31+
* @param tokenAddress - The address of the token
32+
* @param tokenId - The id of the token
33+
* @param value - The value you wish to get the royalty of
34+
*
35+
* returns Two arrays of equal length, royalty recipients and the corresponding amount each recipient should get
36+
*/
37+
function getRoyaltyView(
38+
address tokenAddress,
39+
uint256 tokenId,
40+
uint256 value
41+
) external view returns (address payable[] memory recipients, uint256[] memory amounts);
42+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.0;
3+
4+
/// @author thirdweb
5+
6+
import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
7+
8+
/**
9+
* @dev Read royalty info for a token.
10+
* Supports RoyaltyEngineV1 and RoyaltyRegistry by manifold.xyz.
11+
*/
12+
interface IRoyaltyPayments is IERC165 {
13+
/// @dev Emitted when the address of RoyaltyEngine is set or updated.
14+
event RoyaltyEngineUpdated(address indexed previousAddress, address indexed newAddress);
15+
16+
/**
17+
* Get the royalty for a given token (address, id) and value amount.
18+
*
19+
* @param tokenAddress - The address of the token
20+
* @param tokenId - The id of the token
21+
* @param value - The value you wish to get the royalty of
22+
*
23+
* returns Two arrays of equal length, royalty recipients and the corresponding amount each recipient should get
24+
*/
25+
function getRoyalty(
26+
address tokenAddress,
27+
uint256 tokenId,
28+
uint256 value
29+
) external returns (address payable[] memory recipients, uint256[] memory amounts);
30+
31+
/**
32+
* Set or override RoyaltyEngine address
33+
*
34+
* @param _royaltyEngineAddress - RoyaltyEngineV1 address
35+
*/
36+
function setRoyaltyEngine(address _royaltyEngineAddress) external;
37+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.0;
3+
4+
/// @author thirdweb
5+
6+
import "../interface/IRoyaltyPayments.sol";
7+
import "../interface/IRoyaltyEngineV1.sol";
8+
import { IERC2981 } from "../../eip/interface/IERC2981.sol";
9+
10+
library RoyaltyPaymentsStorage {
11+
bytes32 public constant ROYALTY_PAYMENTS_STORAGE_POSITION = keccak256("royalty.payments.storage");
12+
13+
struct Data {
14+
/// @dev The address of RoyaltyEngineV1, replacing the one set during construction.
15+
address royaltyEngineAddressOverride;
16+
}
17+
18+
function royaltyPaymentsStorage() internal pure returns (Data storage royaltyPaymentsData) {
19+
bytes32 position = ROYALTY_PAYMENTS_STORAGE_POSITION;
20+
assembly {
21+
royaltyPaymentsData.slot := position
22+
}
23+
}
24+
}
25+
26+
/**
27+
* @author thirdweb.com
28+
*
29+
* @title Royalty Payments
30+
* @notice Thirdweb's `RoyaltyPayments` is a contract extension to be used with a marketplace contract.
31+
* It exposes functions for fetching royalty settings for a token.
32+
* It Supports RoyaltyEngineV1 and RoyaltyRegistry by manifold.xyz.
33+
*/
34+
35+
abstract contract RoyaltyPaymentsLogic is IRoyaltyPayments {
36+
// solhint-disable-next-line var-name-mixedcase
37+
address immutable ROYALTY_ENGINE_ADDRESS;
38+
39+
constructor(address _royaltyEngineAddress) {
40+
// allow address(0) in case RoyaltyEngineV1 not present on a network
41+
require(
42+
_royaltyEngineAddress == address(0) ||
43+
IERC165(_royaltyEngineAddress).supportsInterface(type(IRoyaltyEngineV1).interfaceId),
44+
"Doesn't support IRoyaltyEngineV1 interface"
45+
);
46+
47+
ROYALTY_ENGINE_ADDRESS = _royaltyEngineAddress;
48+
}
49+
50+
/**
51+
* Get the royalty for a given token (address, id) and value amount. Does not cache the bps/amounts. Caches the spec for a given token address
52+
*
53+
* @param tokenAddress - The address of the token
54+
* @param tokenId - The id of the token
55+
* @param value - The value you wish to get the royalty of
56+
*
57+
* returns Two arrays of equal length, royalty recipients and the corresponding amount each recipient should get
58+
*/
59+
function getRoyalty(
60+
address tokenAddress,
61+
uint256 tokenId,
62+
uint256 value
63+
) external returns (address payable[] memory recipients, uint256[] memory amounts) {
64+
address royaltyEngineAddress = getRoyaltyEngineAddress();
65+
66+
if (royaltyEngineAddress == address(0)) {
67+
try IERC2981(tokenAddress).royaltyInfo(tokenId, value) returns (address recipient, uint256 amount) {
68+
require(amount < value, "Invalid royalty amount");
69+
70+
recipients = new address payable[](1);
71+
amounts = new uint256[](1);
72+
recipients[0] = payable(recipient);
73+
amounts[0] = amount;
74+
} catch {}
75+
} else {
76+
(recipients, amounts) = IRoyaltyEngineV1(royaltyEngineAddress).getRoyalty(tokenAddress, tokenId, value);
77+
}
78+
}
79+
80+
/**
81+
* Set or override RoyaltyEngine address
82+
*
83+
* @param _royaltyEngineAddress - RoyaltyEngineV1 address
84+
*/
85+
function setRoyaltyEngine(address _royaltyEngineAddress) external {
86+
if (!_canSetRoyaltyEngine()) {
87+
revert("Not authorized");
88+
}
89+
90+
require(
91+
_royaltyEngineAddress != address(0) &&
92+
IERC165(_royaltyEngineAddress).supportsInterface(type(IRoyaltyEngineV1).interfaceId),
93+
"Doesn't support IRoyaltyEngineV1 interface"
94+
);
95+
96+
_setupRoyaltyEngine(_royaltyEngineAddress);
97+
}
98+
99+
/// @dev Returns original or overridden address for RoyaltyEngineV1
100+
function getRoyaltyEngineAddress() public view returns (address royaltyEngineAddress) {
101+
RoyaltyPaymentsStorage.Data storage data = RoyaltyPaymentsStorage.royaltyPaymentsStorage();
102+
address royaltyEngineOverride = data.royaltyEngineAddressOverride;
103+
royaltyEngineAddress = royaltyEngineOverride != address(0) ? royaltyEngineOverride : ROYALTY_ENGINE_ADDRESS;
104+
}
105+
106+
/// @dev Lets a contract admin update the royalty engine address
107+
function _setupRoyaltyEngine(address _royaltyEngineAddress) internal {
108+
RoyaltyPaymentsStorage.Data storage data = RoyaltyPaymentsStorage.royaltyPaymentsStorage();
109+
address currentAddress = data.royaltyEngineAddressOverride;
110+
111+
data.royaltyEngineAddressOverride = _royaltyEngineAddress;
112+
113+
emit RoyaltyEngineUpdated(currentAddress, _royaltyEngineAddress);
114+
}
115+
116+
/// @dev Returns whether royalty engine address can be set in the given execution context.
117+
function _canSetRoyaltyEngine() internal view virtual returns (bool);
118+
}

contracts/marketplace/direct-listings/DirectListingsLogic.sol

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import "../../extension/plugin/PlatformFeeLogic.sol";
1717
import "../../extension/plugin/ERC2771ContextConsumer.sol";
1818
import "../../extension/plugin/ReentrancyGuardLogic.sol";
1919
import "../../extension/plugin/PermissionsEnumerableLogic.sol";
20+
import { RoyaltyPaymentsLogic } from "../../extension/plugin/RoyaltyPayments.sol";
2021
import { CurrencyTransferLib } from "../../lib/CurrencyTransferLib.sol";
2122

2223
/**
@@ -511,46 +512,69 @@ contract DirectListingsLogic is IDirectListings, ReentrancyGuardLogic, ERC2771Co
511512
uint256 _totalPayoutAmount,
512513
Listing memory _listing
513514
) internal {
514-
(address platformFeeRecipient, uint16 platformFeeBps) = PlatformFeeLogic(address(this)).getPlatformFeeInfo();
515-
uint256 platformFeeCut = (_totalPayoutAmount * platformFeeBps) / MAX_BPS;
516-
517-
uint256 royaltyCut;
518-
address royaltyRecipient;
519-
520-
// Distribute royalties. See Sushiswap's https://github.com/sushiswap/shoyu/blob/master/contracts/base/BaseExchange.sol#L296
521-
try IERC2981(_listing.assetContract).royaltyInfo(_listing.tokenId, _totalPayoutAmount) returns (
522-
address royaltyFeeRecipient,
523-
uint256 royaltyFeeAmount
524-
) {
525-
if (royaltyFeeRecipient != address(0) && royaltyFeeAmount > 0) {
526-
require(royaltyFeeAmount + platformFeeCut <= _totalPayoutAmount, "fees exceed the price");
527-
royaltyRecipient = royaltyFeeRecipient;
528-
royaltyCut = royaltyFeeAmount;
515+
address _nativeTokenWrapper = nativeTokenWrapper;
516+
uint256 amountRemaining;
517+
518+
// Payout platform fee
519+
{
520+
(address platformFeeRecipient, uint16 platformFeeBps) = PlatformFeeLogic(address(this))
521+
.getPlatformFeeInfo();
522+
uint256 platformFeeCut = (_totalPayoutAmount * platformFeeBps) / MAX_BPS;
523+
524+
// Transfer platform fee
525+
CurrencyTransferLib.transferCurrencyWithWrapper(
526+
_currencyToUse,
527+
_payer,
528+
platformFeeRecipient,
529+
platformFeeCut,
530+
_nativeTokenWrapper
531+
);
532+
533+
amountRemaining = _totalPayoutAmount - platformFeeCut;
534+
}
535+
536+
// Payout royalties
537+
{
538+
// Get royalty recipients and amounts
539+
(address payable[] memory recipients, uint256[] memory amounts) = RoyaltyPaymentsLogic(address(this))
540+
.getRoyalty(_listing.assetContract, _listing.tokenId, _totalPayoutAmount);
541+
542+
uint256 royaltyRecipientCount = recipients.length;
543+
544+
if (royaltyRecipientCount != 0) {
545+
uint256 royaltyCut;
546+
address royaltyRecipient;
547+
548+
for (uint256 i = 0; i < royaltyRecipientCount; ) {
549+
royaltyRecipient = recipients[i];
550+
royaltyCut = amounts[i];
551+
552+
// Check payout amount remaining is enough to cover royalty payment
553+
require(amountRemaining >= royaltyCut, "fees exceed the price");
554+
555+
// Transfer royalty
556+
CurrencyTransferLib.transferCurrencyWithWrapper(
557+
_currencyToUse,
558+
_payer,
559+
royaltyRecipient,
560+
royaltyCut,
561+
_nativeTokenWrapper
562+
);
563+
564+
unchecked {
565+
amountRemaining -= royaltyCut;
566+
++i;
567+
}
568+
}
529569
}
530-
} catch {}
570+
}
531571

532572
// Distribute price to token owner
533-
address _nativeTokenWrapper = nativeTokenWrapper;
534-
535-
CurrencyTransferLib.transferCurrencyWithWrapper(
536-
_currencyToUse,
537-
_payer,
538-
platformFeeRecipient,
539-
platformFeeCut,
540-
_nativeTokenWrapper
541-
);
542-
CurrencyTransferLib.transferCurrencyWithWrapper(
543-
_currencyToUse,
544-
_payer,
545-
royaltyRecipient,
546-
royaltyCut,
547-
_nativeTokenWrapper
548-
);
549573
CurrencyTransferLib.transferCurrencyWithWrapper(
550574
_currencyToUse,
551575
_payer,
552576
_payee,
553-
_totalPayoutAmount - (platformFeeCut + royaltyCut),
577+
amountRemaining,
554578
_nativeTokenWrapper
555579
);
556580
}

0 commit comments

Comments
 (0)