Skip to content
This repository has been archived by the owner on Apr 30, 2024. It is now read-only.

Add royalty setting flexibility #67

Merged
merged 14 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions contracts/interfaces/modules/royalty/IRoyaltyModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ interface IRoyaltyModule {
/// @param amount The amount that is paid
event RoyaltyPaid(address receiverIpId, address payerIpId, address sender, address token, uint256 amount);

/// @notice Sets the license registry
/// @param licenseRegistry The address of the license registry
function setLicenseRegistry(address licenseRegistry) external;

/// @notice Whitelist a royalty policy
/// @param royaltyPolicy The address of the royalty policy
/// @param allowed Indicates if the royalty policy is whitelisted or not
Expand Down
8 changes: 3 additions & 5 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ library Errors {
error LicenseRegistry__CallerNotLicensorAndPolicyNotSet();
error LicenseRegistry__DerivativesCannotAddPolicy();
error LicenseRegistry__IncompatibleLicensorRoyaltyPolicy();
error LicenseRegistry__DerivativeRevShareSumExceedsMaxRNFTSupply();

////////////////////////////////////////////////////////////////////////////
// LicenseRegistryAware //
Expand Down Expand Up @@ -181,8 +182,8 @@ library Errors {
error RoyaltyModule__NotWhitelistedRoyaltyToken();
error RoyaltyModule__NoRoyaltyPolicySet();
error RoyaltyModule__IncompatibleRoyaltyPolicy();
error RoyaltyModule__NotRegistrationModule();
error RoyaltyModule__ZeroLicensingModule();
error RoyaltyModule__NotAllowedCaller();
error RoyaltyModule__ZeroLicenseRegistry();

error RoyaltyPolicyLS__ZeroRoyaltyModule();
error RoyaltyPolicyLS__ZeroLiquidSplitFactory();
Expand All @@ -198,14 +199,11 @@ library Errors {
error LSClaimer__InvalidPathFirstPosition();
error LSClaimer__InvalidPathLastPosition();
error LSClaimer__AlreadyClaimed();
error LSClaimer__ZeroRNFT();
error LSClaimer__RNFTAlreadySet();
error LSClaimer__ETHBalanceNotZero();
error LSClaimer__ERC20BalanceNotZero();
error LSClaimer__ZeroIpId();
error LSClaimer__ZeroLicenseRegistry();
error LSClaimer__ZeroRoyaltyPolicyLS();
error LSClaimer__NotRoyaltyPolicyLS();

////////////////////////////////////////////////////////////////////////////
// ModuleRegistry //
Expand Down
32 changes: 18 additions & 14 deletions contracts/modules/royalty-module/RoyaltyModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { Errors } from "contracts/lib/Errors.sol";
/// @notice The Story Protocol royalty module allows to set royalty policies an ipId
/// and pay royalties as a derivative ip.
contract RoyaltyModule is IRoyaltyModule, Governable, ReentrancyGuard {
/// @notice registration module address
address public REGISTRATION_MODULE;
/// @notice License registry address
address public LICENSE_REGISTRY;

/// @notice Indicates if a royalty policy is whitelisted
mapping(address royaltyPolicy => bool allowed) public isWhitelistedRoyaltyPolicy;
Expand All @@ -25,19 +25,19 @@ contract RoyaltyModule is IRoyaltyModule, Governable, ReentrancyGuard {
/// @notice Indicates the royalty policy for a given ipId
mapping(address ipId => address royaltyPolicy) public royaltyPolicies;

/// @notice Restricts the calls to the license module
modifier onlyRegistrationModule() {
// TODO: if (msg.sender != REGISTRATION_MODULE) revert Errors.RoyaltyModule__NotRegistrationModule();
_;
}
/// @notice Indicates if a royalty policy is immutable
mapping(address ipId => bool) public isRoyaltyPolicyImmutable;

/// @notice Constructor
/// @param _governance The address of the governance contract
constructor(address _governance) Governable(_governance) {}

function initialize(address _registrationModule) external onlyProtocolAdmin {
if (_registrationModule == address(0)) revert Errors.RoyaltyModule__ZeroLicensingModule();
REGISTRATION_MODULE = _registrationModule;
/// @notice Sets the license registry
/// @param _licenseRegistry The address of the license registry
function setLicenseRegistry(address _licenseRegistry) external onlyProtocolAdmin {
if (_licenseRegistry == address(0)) revert Errors.RoyaltyModule__ZeroLicenseRegistry();

LICENSE_REGISTRY = _licenseRegistry;
}

/// @notice Whitelist a royalty policy
Expand All @@ -62,24 +62,28 @@ contract RoyaltyModule is IRoyaltyModule, Governable, ReentrancyGuard {
emit RoyaltyTokenWhitelistUpdated(_token, _allowed);
}

// TODO: Ensure this function is called on ipId registration: root and derivatives registrations
// TODO: Ensure that the ipId that is passed in from license cannot be manipulated - given ipId addresses are deterministic
Ramarti marked this conversation as resolved.
Show resolved Hide resolved
/// @notice Sets the royalty policy for an ipId
/// @param _ipId The ipId
/// @param _royaltyPolicy The address of the royalty policy
/// @param _parentIpIds The parent ipIds
/// @param _data The data to initialize the policy
function setRoyaltyPolicy(
address _ipId,
address _ipId,
address _royaltyPolicy,
address[] calldata _parentIpIds,
bytes calldata _data
) external onlyRegistrationModule nonReentrant {
if (royaltyPolicies[_ipId] != address(0)) revert Errors.RoyaltyModule__AlreadySetRoyaltyPolicy();
) external nonReentrant {
if (msg.sender != LICENSE_REGISTRY) revert Errors.RoyaltyModule__NotAllowedCaller();
if (isRoyaltyPolicyImmutable[_ipId]) revert Errors.RoyaltyModule__AlreadySetRoyaltyPolicy();
if (!isWhitelistedRoyaltyPolicy[_royaltyPolicy]) revert Errors.RoyaltyModule__NotWhitelistedRoyaltyPolicy();

if (_parentIpIds.length > 0) isRoyaltyPolicyImmutable[_ipId] = true;

// the loop below is limited to 100 iterations
for (uint32 i = 0; i < _parentIpIds.length; i++) {
if (royaltyPolicies[_parentIpIds[i]] != _royaltyPolicy) revert Errors.RoyaltyModule__IncompatibleRoyaltyPolicy();
isRoyaltyPolicyImmutable[_parentIpIds[i]] = true;
Ramarti marked this conversation as resolved.
Show resolved Hide resolved
}

royaltyPolicies[_ipId] = _royaltyPolicy;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { Errors } from "contracts/lib/Errors.sol";
/// the percentage of royalty NFTs owned by each account.
contract RoyaltyPolicyLS is IRoyaltyPolicyLS, ERC1155Holder {
using SafeERC20 for IERC20;

struct LSRoyaltyData {
address splitClone; // address of the liquid split clone contract for a given ipId
address claimer; // address of the claimer contract for a given ipId
Expand Down Expand Up @@ -68,7 +68,6 @@ contract RoyaltyPolicyLS is IRoyaltyPolicyLS, ERC1155Holder {
LIQUID_SPLIT_MAIN = _liquidSplitMain;
}

// TODO: Ensure that parentsIds should be correctly passed in through the licensing contract, otherwise we must call parents() on licenseRegistry directly
/// @notice Initializes the royalty policy
/// @param _ipId The ipId
/// @param _parentIpIds The parent ipIds
Expand Down
40 changes: 30 additions & 10 deletions contracts/registries/LicenseRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry, AccessControlled {
// When a child passes in a royalty policy
address royaltyPolicyAddress = address(0);
uint32 royaltyDerivativeRevShare = 0;
uint32 derivativeRevShareSum = 0;

for (uint256 i = 0; i < licenseIds.length; i++) {
uint256 licenseId = licenseIds[i];
Expand All @@ -193,19 +194,23 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry, AccessControlled {
}
Licensing.License memory licenseData = _licenses[licenseId];
licensors[i] = licenseData.licensorIpId;
(royaltyPolicyAddress, royaltyDerivativeRevShare) = _linkIpToParent(

(royaltyPolicyAddress, royaltyDerivativeRevShare, derivativeRevShareSum) = _linkIpToParent(
i,
licenseId,
licenseData,
licensors[i],
childIpId,
royaltyPolicyAddress
royaltyPolicyAddress,
derivativeRevShareSum
);

values[i] = 1;
}
emit IpIdLinkedToParents(msg.sender, childIpId, licensors);

// Licenses unanimously require royalty.
// TODO: currently, `royaltyDerivativeRevShare` is the derivative rev share value of the last license.
if (royaltyPolicyAddress != address(0)) {
ROYALTY_MODULE.setRoyaltyPolicy(
childIpId,
Expand All @@ -225,8 +230,9 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry, AccessControlled {
Licensing.License memory licenseData,
address licensor,
address childIpId,
address royaltyPolicyAddress
) private returns (address nextRoyaltyPolicyAddress, uint32 royaltyDerivativeRevShare) {
address royaltyPolicyAddress,
uint32 derivativeRevShareSum
) private returns (address nextRoyaltyPolicyAddress, uint32 nextRoyaltyDerivativeRevShare, uint32 nextDerivativeRevShareSum) {
// TODO: check licensor not part of a branch tagged by disputer
if (licensor == childIpId) {
revert Errors.LicenseRegistry__ParentIdEqualThanChild();
Expand All @@ -248,21 +254,35 @@ contract LicenseRegistry is ERC1155, ILicenseRegistry, AccessControlled {

// If link says royalty is required for license (licenseIds[i]) and no royalty policy is set, set it.
// But if the index is NOT 0, this is previous licenses didn't set the royalty policy because they don't
// require royalty payment. So, revert in this case.
if (response.isRoyaltyRequired && royaltyPolicyAddress == address(0)) {
if (iteration != 0) {
// require royalty payment. So, revert in this case. Similarly, if the new royaltyPolicyAddress is different
// from the previous one (in iteration > 0), revert. We currently restrict all licenses (parents) to have
// the same royalty policy, so the child can inherit it.
if (response.isRoyaltyRequired) {
if (iteration > 0 && royaltyPolicyAddress != response.royaltyPolicy) {
// If iteration > 0 and
// - royaltyPolicyAddress == address(0), revert. Previous licenses didn't set RP.
// - royaltyPolicyAddress != response.royaltyPolicy, revert. Previous licenses set different RP.
// ==> this can be considered as royaltyPolicyAddress != response.royaltyPolicy
revert Errors.LicenseRegistry__IncompatibleLicensorRoyaltyPolicy();
}
royaltyPolicyAddress = response.royaltyPolicy;
royaltyDerivativeRevShare = response.royaltyDerivativeRevShare;

// TODO: Read max RNFT supply instead of hardcoding the expected max supply
// TODO: Do we need safe check?
// TODO: Test this in unit test.
if (derivativeRevShareSum + response.royaltyDerivativeRevShare > 1000) {
revert Errors.LicenseRegistry__DerivativeRevShareSumExceedsMaxRNFTSupply();
}

nextRoyaltyPolicyAddress = response.royaltyPolicy;
nextRoyaltyDerivativeRevShare = response.royaltyDerivativeRevShare;
nextDerivativeRevShareSum = derivativeRevShareSum + response.royaltyDerivativeRevShare;
}

// Add the policy of licenseIds[i] to the child. If the policy's already set from previous parents,
// then the addition will be skipped.
_addPolicyIdToIp({ ipId: childIpId, policyId: licenseData.policyId, isInherited: true, skipIfDuplicate: true });
// Set parent
_ipIdParents[childIpId].add(licensor);
return (royaltyPolicyAddress, royaltyDerivativeRevShare);
}

/// @notice True if the framework address is registered in LicenseRegistry
Expand Down
2 changes: 1 addition & 1 deletion script/foundry/deployment/Main.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ contract Main is Script, BroadcastManager, JsonDeploymentHandler {

function _configInitialize() private {
accessController.initialize(address(ipAssetRegistry), address(moduleRegistry));
royaltyModule.initialize(address(registrationModule));
royaltyModule.setLicenseRegistry(address(licenseRegistry));
}

function _configureModuleRegistry() private {
Expand Down
2 changes: 1 addition & 1 deletion test/foundry/integration/BaseIntegration.sol
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ contract BaseIntegration is Test {
function _configDeployedContracts() internal {
vm.startPrank(u.admin);
accessController.initialize(address(ipAccountRegistry), address(moduleRegistry));
royaltyModule.initialize(address(registrationModule));
royaltyModule.setLicenseRegistry(address(licenseRegistry));

moduleRegistry.registerModule(REGISTRATION_MODULE_KEY, address(registrationModule));
moduleRegistry.registerModule(IP_RESOLVER_MODULE_KEY, address(ipResolver));
Expand Down
4 changes: 3 additions & 1 deletion test/foundry/integration/big-bang/NftLicenseRoyalty.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ contract BigBang_Integration_NftLicenseRoyalty is BaseIntegration, Integration_S

// Alice sets royalty policy on her root IP
{
// TODO: setRoyaltyPolicy should be called through mintLicense or addPolicyToIp, not directly by user
vm.startPrank(address(licenseRegistry));
royaltyModule.setRoyaltyPolicy(
ipAcct[1],
address(royaltyPolicyLS),
Expand Down Expand Up @@ -293,5 +295,5 @@ contract BigBang_Integration_NftLicenseRoyalty is BaseIntegration, Integration_S
_tokens: tokens
});
}
}
}
}
8 changes: 6 additions & 2 deletions test/foundry/integration/big-bang/SingleNftCollection.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ contract BigBang_Integration_SingleNftCollection is BaseIntegration, Integration

// Alice sets royalty policy for her root IPAccounts
// (so other IPAccounts can use her policies that inits royalty policy on linking)
// TODO: setRoyaltyPolicy should be called through mintLicense or addPolicyToIp, not directly by user
vm.startPrank(address(licenseRegistry));
royaltyModule.setRoyaltyPolicy(
ipAcct[1],
address(royaltyPolicyLS),
Expand All @@ -137,6 +139,8 @@ contract BigBang_Integration_SingleNftCollection is BaseIntegration, Integration

// Bob sets royalty policy for his root IPAccounts
// (so other IPAccounts can use his policies that inits royalty policy on linking)
// TODO: setRoyaltyPolicy should be called through mintLicense or addPolicyToIp, not directly by user
vm.startPrank(address(licenseRegistry));
royaltyModule.setRoyaltyPolicy(
ipAcct[300],
address(royaltyPolicyLS),
Expand All @@ -161,7 +165,7 @@ contract BigBang_Integration_SingleNftCollection is BaseIntegration, Integration
MINT & USE LICENSES
////////////////////////////////////////////////////////////////*/

/* // Carl mints 1 license for policy "com_deriv_all_true" on Alice's NFT 1 IPAccount
// Carl mints 1 license for policy "com_deriv_all_true" on Alice's NFT 1 IPAccount
// Carl creates NFT 6 IPAccount
// Carl activates the license on his NFT 6 IPAccount, linking as child to Alice's NFT 1 IPAccount
{
Expand Down Expand Up @@ -309,6 +313,6 @@ contract BigBang_Integration_SingleNftCollection is BaseIntegration, Integration
metadata,
u.carl // caller
);
} */
}
}
}
2 changes: 1 addition & 1 deletion test/foundry/modules/ModuleBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ abstract contract ModuleBaseTest is BaseTest {
);
baseModule = IModule(_deployModule());
accessController.initialize(address(ipAssetRegistry), address(moduleRegistry));
royaltyModule.initialize(address(registrationModule));
royaltyModule.setLicenseRegistry(address(licenseRegistry));
}

/// @notice Tests that the default resolver constructor runs successfully.
Expand Down
7 changes: 3 additions & 4 deletions test/foundry/modules/royalty/LSClaimer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ contract TestLSClaimer is TestHelper {
function setUp() public override {
TestHelper.setUp();
_setUMLPolicyFrameworkManager();
nft = new MockERC721("mock");
nft = erc721.ape;
_addUMLPolicy(
true,
true,
Expand Down Expand Up @@ -102,15 +102,14 @@ contract TestLSClaimer is TestHelper {
);
vm.label(ipAddr, string(abi.encodePacked("IPAccount", Strings.toString(nftIds[0]))));
vm.stopPrank();
// User sets royalty policy for her IPAccount (so other IPAccounts can use her policies that
// inits royalty policy on linking)
vm.startPrank(address(registrationModule));
vm.startPrank(address(licenseRegistry));
royaltyModule.setRoyaltyPolicy(
ipAddr,
address(royaltyPolicyLS),
new address[](0), // no parent
abi.encode(10)
);

vm.stopPrank();
vm.startPrank(deployer);
uint256[] memory licenseId = new uint256[](1);
Expand Down
Loading
Loading