Royalties
TL Creator Contracts utilize EIP-2981 to configure royalties on-chain.
Last updated
TL Creator Contracts utilize EIP-2981 to configure royalties on-chain.
Last updated
All TL Creator Contracts implement EIP-2981 compliant royalties. The implementation we created allows for a default royalty configuration set at the contract level and then individual token overrides.
Only the owner of the contract can edit the royalty configuration either at the contract or token level.
It is important to note that individual token royalties are respected at marketplaces like SuperRare and Foundation, but NOT respected by OpenSea at this time. We have asked them about it and it is on the roadmap, but until then, the default royalty configuration is used.
The code utilized by the Creator Contracts can be found below.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC165Upgradeable} from "@openzeppelin-contracts-upgradeable-5.0.2/utils/introspection/ERC165Upgradeable.sol";
import {IEIP2981} from "../../royalties/IEIP2981.sol";
/// @title EIP2981TLUpgradeable.sol
/// @notice Abstract contract to define a default royalty spec
/// while allowing for specific token overrides
/// @dev Follows EIP-2981 (https://eips.ethereum.org/EIPS/eip-2981)
/// @author transientlabs.xyz
/// @custom:version 3.1.0
abstract contract EIP2981TLUpgradeable is IEIP2981, ERC165Upgradeable {
/*//////////////////////////////////////////////////////////////////////////
Types
//////////////////////////////////////////////////////////////////////////*/
struct RoyaltySpec {
address recipient;
uint256 percentage;
}
/*//////////////////////////////////////////////////////////////////////////
Storage
//////////////////////////////////////////////////////////////////////////*/
/// @custom:storage-location erc7201:transientlabs.storage.EIP2981TLStorage
struct EIP2981TLStorage {
address defaultRecipient;
uint256 defaultPercentage;
mapping(uint256 => RoyaltySpec) tokenOverrides;
}
// keccak256(abi.encode(uint256(keccak256("transientlabs.storage.EIP2981TLStorage")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant EIP2981TLStorageLocation =
0xe9db8e9b56f2e28e12956850f386d9a4c1e886a4f584b61a10a9d0cacee70700;
function _getEIP2981TLStorage() private pure returns (EIP2981TLStorage storage $) {
assembly {
$.slot := EIP2981TLStorageLocation
}
}
/*//////////////////////////////////////////////////////////////////////////
Constants
//////////////////////////////////////////////////////////////////////////*/
uint256 public constant BASIS = 10_000;
/*//////////////////////////////////////////////////////////////////////////
Events
//////////////////////////////////////////////////////////////////////////*/
/// @dev Event to emit when the default roylaty is updated
event DefaultRoyaltyUpdate(address indexed sender, address newRecipient, uint256 newPercentage);
/// @dev Event to emit when a token royalty is overriden
event TokenRoyaltyOverride(
address indexed sender, uint256 indexed tokenId, address newRecipient, uint256 newPercentage
);
/*//////////////////////////////////////////////////////////////////////////
Errors
//////////////////////////////////////////////////////////////////////////*/
/// @dev error if the recipient is set to address(0)
error ZeroAddressError();
/// @dev error if the royalty percentage is greater than to 100%
error MaxRoyaltyError();
/*//////////////////////////////////////////////////////////////////////////
Initializer
//////////////////////////////////////////////////////////////////////////*/
/// @notice Function to initialize the contract
/// @param defaultRecipient The default royalty payout address
/// @param defaultPercentage The deafult royalty percentage, out of 10,000
function __EIP2981TL_init(address defaultRecipient, uint256 defaultPercentage) internal onlyInitializing {
__EIP2981TL_init_unchained(defaultRecipient, defaultPercentage);
}
/// @notice Unchained function to initialize the contract
/// @param defaultRecipient The default royalty payout address
/// @param defaultPercentage The deafult royalty percentage, out of 10,000
function __EIP2981TL_init_unchained(address defaultRecipient, uint256 defaultPercentage)
internal
onlyInitializing
{
_setDefaultRoyaltyInfo(defaultRecipient, defaultPercentage);
}
/*//////////////////////////////////////////////////////////////////////////
Royalty Changing Functions
//////////////////////////////////////////////////////////////////////////*/
/// @notice Function to set default royalty info
/// @param newRecipient The new default royalty payout address
/// @param newPercentage The new default royalty percentage, out of 10,000
function _setDefaultRoyaltyInfo(address newRecipient, uint256 newPercentage) internal {
EIP2981TLStorage storage $ = _getEIP2981TLStorage();
if (newRecipient == address(0)) revert ZeroAddressError();
if (newPercentage > 10_000) revert MaxRoyaltyError();
$.defaultRecipient = newRecipient;
$.defaultPercentage = newPercentage;
emit DefaultRoyaltyUpdate(msg.sender, newRecipient, newPercentage);
}
/// @notice Function to override royalty spec on a specific token
/// @param tokenId The token id to override royalty for
/// @param newRecipient The new royalty payout address
/// @param newPercentage The new royalty percentage, out of 10,000
function _overrideTokenRoyaltyInfo(uint256 tokenId, address newRecipient, uint256 newPercentage) internal {
EIP2981TLStorage storage $ = _getEIP2981TLStorage();
if (newRecipient == address(0)) revert ZeroAddressError();
if (newPercentage > 10_000) revert MaxRoyaltyError();
$.tokenOverrides[tokenId].recipient = newRecipient;
$.tokenOverrides[tokenId].percentage = newPercentage;
emit TokenRoyaltyOverride(msg.sender, tokenId, newRecipient, newPercentage);
}
/*//////////////////////////////////////////////////////////////////////////
Royalty Info
//////////////////////////////////////////////////////////////////////////*/
/// @inheritdoc IEIP2981
function royaltyInfo(uint256 tokenId, uint256 salePrice)
external
view
returns (address receiver, uint256 royaltyAmount)
{
EIP2981TLStorage storage $ = _getEIP2981TLStorage();
address recipient = $.defaultRecipient;
uint256 percentage = $.defaultPercentage;
if ($.tokenOverrides[tokenId].recipient != address(0)) {
recipient = $.tokenOverrides[tokenId].recipient;
percentage = $.tokenOverrides[tokenId].percentage;
}
return (recipient, salePrice * percentage / BASIS);
}
/*//////////////////////////////////////////////////////////////////////////
ERC-165 Override
//////////////////////////////////////////////////////////////////////////*/
/// @inheritdoc ERC165Upgradeable
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165Upgradeable) returns (bool) {
return interfaceId == type(IEIP2981).interfaceId || ERC165Upgradeable.supportsInterface(interfaceId);
}
/*//////////////////////////////////////////////////////////////////////////
External View Functions
//////////////////////////////////////////////////////////////////////////*/
/// @notice Query the default royalty receiver and percentage.
/// @return Tuple containing the default royalty recipient and percentage out of 10_000
function getDefaultRoyaltyRecipientAndPercentage() external view returns (address, uint256) {
EIP2981TLStorage storage $ = _getEIP2981TLStorage();
return ($.defaultRecipient, $.defaultPercentage);
}
}