diff --git a/contracts/interfaces/modules/royalty/IRoyaltyModule.sol b/contracts/interfaces/modules/royalty/IRoyaltyModule.sol index 1cee0596..81a014cd 100644 --- a/contracts/interfaces/modules/royalty/IRoyaltyModule.sol +++ b/contracts/interfaces/modules/royalty/IRoyaltyModule.sol @@ -30,9 +30,18 @@ interface IRoyaltyModule is IModule { /// @param amount The amount paid event LicenseMintingFeePaid(address receiverIpId, address payerAddress, address token, uint256 amount); + /// @notice Sets the license registry + /// @dev Enforced to be only callable by the protocol admin + /// @param licensing The address of the license registry + /// @param dispute The address of the dispute module + function setLicensingAndDisputeModules(address licensing, address dispute) external; + /// @notice Returns the licensing module address function licensingModule() external view returns (address); + /// @notice Returns the dispute module address + function disputeModule() external view returns (address); + /// @notice Indicates if a royalty policy is whitelisted /// @param royaltyPolicy The address of the royalty policy /// @return isWhitelisted True if the royalty policy is whitelisted diff --git a/contracts/modules/royalty/RoyaltyModule.sol b/contracts/modules/royalty/RoyaltyModule.sol index 9a34d305..01cffefd 100644 --- a/contracts/modules/royalty/RoyaltyModule.sol +++ b/contracts/modules/royalty/RoyaltyModule.sol @@ -10,6 +10,7 @@ import { BaseModule } from "../BaseModule.sol"; import { GovernableUpgradeable } from "../../governance/GovernableUpgradeable.sol"; import { IRoyaltyModule } from "../../interfaces/modules/royalty/IRoyaltyModule.sol"; import { IRoyaltyPolicy } from "../../interfaces/modules/royalty/policies/IRoyaltyPolicy.sol"; +import { IDisputeModule } from "../../interfaces/modules/dispute/IDisputeModule.sol"; import { Errors } from "../../lib/Errors.sol"; import { ROYALTY_MODULE_KEY } from "../../lib/modules/Module.sol"; import { BaseModule } from "../BaseModule.sol"; @@ -27,12 +28,14 @@ contract RoyaltyModule is using ERC165Checker for address; /// @dev Storage structure for the RoyaltyModule + /// @param disputeModule The address of the dispute module /// @param licensingModule The address of the licensing module /// @param isWhitelistedRoyaltyPolicy Indicates if a royalty policy is whitelisted /// @param isWhitelistedRoyaltyToken Indicates if a royalty token is whitelisted /// @param royaltyPolicies Indicates the royalty policy for a given IP asset /// @custom:storage-location erc7201:story-protocol.RoyaltyModule struct RoyaltyModuleStorage { + address disputeModule; address licensingModule; mapping(address royaltyPolicy => bool isWhitelisted) isWhitelistedRoyaltyPolicy; mapping(address token => bool) isWhitelistedRoyaltyToken; @@ -69,9 +72,13 @@ contract RoyaltyModule is /// @notice Sets the license registry /// @dev Enforced to be only callable by the protocol admin /// @param licensing The address of the license registry - function setLicensingModule(address licensing) external onlyProtocolAdmin { + /// @param dispute The address of the dispute module + function setLicensingAndDisputeModules(address licensing, address dispute) external onlyProtocolAdmin { if (licensing == address(0)) revert Errors.RoyaltyModule__ZeroLicensingModule(); + if (dispute == address(0)) revert Errors.RoyaltyModule__ZeroDisputeModule(); + _getRoyaltyModuleStorage().licensingModule = licensing; + _getRoyaltyModuleStorage().disputeModule = dispute; } /// @notice Whitelist a royalty policy @@ -175,6 +182,10 @@ contract RoyaltyModule is RoyaltyModuleStorage storage $ = _getRoyaltyModuleStorage(); if (!$.isWhitelistedRoyaltyToken[token]) revert Errors.RoyaltyModule__NotWhitelistedRoyaltyToken(); + IDisputeModule dispute = IDisputeModule($.disputeModule); + if (dispute.isIpTagged(receiverIpId)) revert Errors.RoyaltyModule__IpIsTagged(); + if (dispute.isIpTagged(payerIpId)) revert Errors.RoyaltyModule__IpIsTagged(); + address payerRoyaltyPolicy = $.royaltyPolicies[payerIpId]; // if the payer does not have a royalty policy set, then the payer is not a derivative ip and does not pay // royalties while the receiver ip can have a zero royalty policy since that could mean it is an ip a root @@ -202,7 +213,7 @@ contract RoyaltyModule is ) external onlyLicensingModule { RoyaltyModuleStorage storage $ = _getRoyaltyModuleStorage(); if (!$.isWhitelistedRoyaltyToken[token]) revert Errors.RoyaltyModule__NotWhitelistedRoyaltyToken(); - + if (IDisputeModule($.disputeModule).isIpTagged(receiverIpId)) revert Errors.RoyaltyModule__IpIsTagged(); if (licenseRoyaltyPolicy == address(0)) revert Errors.RoyaltyModule__NoRoyaltyPolicySet(); if (!$.isWhitelistedRoyaltyPolicy[licenseRoyaltyPolicy]) revert Errors.RoyaltyModule__NotWhitelistedRoyaltyPolicy(); @@ -217,6 +228,11 @@ contract RoyaltyModule is return _getRoyaltyModuleStorage().licensingModule; } + /// @notice Returns the dispute module address + function disputeModule() external view returns (address) { + return _getRoyaltyModuleStorage().disputeModule; + } + /// @notice Indicates if a royalty policy is whitelisted /// @param royaltyPolicy The address of the royalty policy /// @return isWhitelisted True if the royalty policy is whitelisted diff --git a/test/foundry/modules/royalty/RoyaltyModule.t.sol b/test/foundry/modules/royalty/RoyaltyModule.t.sol index fc3b4553..1edc2b4a 100644 --- a/test/foundry/modules/royalty/RoyaltyModule.t.sol +++ b/test/foundry/modules/royalty/RoyaltyModule.t.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.23; +import { ERC6551AccountLib } from "erc6551/lib/ERC6551AccountLib.sol"; + // contracts import { Errors } from "../../../../contracts/lib/Errors.sol"; import { RoyaltyModule } from "../../../../contracts/modules/royalty/RoyaltyModule.sol"; import { RoyaltyPolicyLAP } from "../../../../contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol"; +import { PILPolicy } from "contracts/modules/licensing/PILPolicyFrameworkManager.sol"; import { TestProxyHelper } from "test/foundry/utils/TestProxyHelper.sol"; // tests @@ -19,6 +22,9 @@ contract TestRoyaltyModule is BaseTest { address internal ipAccount1 = address(0x111000aaa); address internal ipAccount2 = address(0x111000bbb); + address internal ipAddr; + address internal arbitrationRelayer; + struct InitParams { address[] targetAncestors; uint32[] targetRoyaltyAmount; @@ -39,9 +45,9 @@ contract TestRoyaltyModule is BaseTest { function setUp() public override { super.setUp(); buildDeployModuleCondition( - DeployModuleCondition({ disputeModule: false, royaltyModule: true, licensingModule: false }) + DeployModuleCondition({ disputeModule: true, royaltyModule: true, licensingModule: false }) ); - buildDeployPolicyCondition(DeployPolicyCondition({ arbitrationPolicySP: false, royaltyPolicyLAP: true })); + buildDeployPolicyCondition(DeployPolicyCondition({ arbitrationPolicySP: true, royaltyPolicyLAP: true })); deployConditionally(); postDeploymentSetup(); @@ -52,6 +58,8 @@ contract TestRoyaltyModule is BaseTest { TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(RoyaltyPolicyLAP.initialize, (getGovernance()))) ); + arbitrationRelayer = u.admin; + vm.startPrank(u.admin); // whitelist royalty policy royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLAP), true); @@ -64,6 +72,51 @@ contract TestRoyaltyModule is BaseTest { // split made to avoid stack too deep error _setupTree(); vm.stopPrank(); + + USDC.mint(ipAccount1, 1000 * 10 ** 6); + + _setPILPolicyFrameworkManager(); + _addPILPolicy( + "cheap_flexible", + true, + address(royaltyPolicyLAP), + PILPolicy({ + attribution: false, + commercialUse: true, + commercialAttribution: true, + commercializerChecker: address(0), + commercializerCheckerData: "", + commercialRevShare: 10, + derivativesAllowed: true, + derivativesAttribution: true, + derivativesApproval: false, + derivativesReciprocal: false, + territories: new string[](0), + distributionChannels: new string[](0), + contentRestrictions: new string[](0) + }) + ); + + mockNFT.mintId(u.alice, 0); + + address expectedAddr = ERC6551AccountLib.computeAddress( + address(erc6551Registry), + address(ipAccountImpl), + ipAccountRegistry.IP_ACCOUNT_SALT(), + block.chainid, + address(mockNFT), + 0 + ); + vm.label(expectedAddr, "IPAccount0"); + + vm.startPrank(u.alice); + ipAddr = ipAssetRegistry.register(address(mockNFT), 0); + licensingModule.addPolicyToIp(ipAddr, policyIds["pil_cheap_flexible"]); + + // set arbitration policy + vm.startPrank(ipAddr); + disputeModule.setArbitrationPolicy(ipAddr, address(arbitrationPolicySP)); + vm.stopPrank(); } function _setupTree() internal { @@ -119,24 +172,35 @@ contract TestRoyaltyModule is BaseTest { royaltyModule.onLinkToParents(address(3), address(royaltyPolicyLAP), parents, encodedLicenseData, encodedBytes); } - function test_RoyaltyModule_setLicensingModule_revert_ZeroLicensingModule() public { + function test_RoyaltyModule_setLicensingAndDisputeModules_revert_ZeroLicensingModule() public { address impl = address(new RoyaltyModule()); RoyaltyModule testRoyaltyModule = RoyaltyModule( TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(RoyaltyModule.initialize, (address(getGovernance())))) ); vm.expectRevert(Errors.RoyaltyModule__ZeroLicensingModule.selector); vm.prank(u.admin); - testRoyaltyModule.setLicensingModule(address(0)); + testRoyaltyModule.setLicensingAndDisputeModules(address(0), address(1)); + } + + function test_RoyaltyModule_setLicensingAndDisputeModules_revert_ZeroDisputeModule() public { + address impl = address(new RoyaltyModule()); + RoyaltyModule testRoyaltyModule = RoyaltyModule( + TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(RoyaltyModule.initialize, (address(getGovernance())))) + ); + vm.expectRevert(Errors.RoyaltyModule__ZeroDisputeModule.selector); + vm.prank(u.admin); + testRoyaltyModule.setLicensingAndDisputeModules(address(1), address(0)); } - function test_RoyaltyModule_setLicensingModule() public { + function test_RoyaltyModule_setLicensingAndDisputeModules() public { vm.startPrank(u.admin); address impl = address(new RoyaltyModule()); RoyaltyModule testRoyaltyModule = RoyaltyModule( TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(RoyaltyModule.initialize, (address(getGovernance())))) ); - testRoyaltyModule.setLicensingModule(address(licensingModule)); + testRoyaltyModule.setLicensingAndDisputeModules(address(licensingModule), address(disputeModule)); assertEq(testRoyaltyModule.licensingModule(), address(licensingModule)); + assertEq(testRoyaltyModule.disputeModule(), address(disputeModule)); } function test_RoyaltyModule_whitelistRoyaltyPolicy_revert_ZeroRoyaltyToken() public { @@ -449,6 +513,24 @@ contract TestRoyaltyModule is BaseTest { royaltyModule.payRoyaltyOnBehalf(receiverIpId, payerIpId, address(1), royaltyAmount); } + function test_RoyaltyModule_payRoyaltyOnBehalf_revert_IpIsTagged() public { + // raise dispute + vm.startPrank(ipAccount1); + USDC.approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(ipAddr, string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); + + // set dispute judgement + vm.startPrank(arbitrationRelayer); + disputeModule.setDisputeJudgement(1, true, ""); + + vm.expectRevert(Errors.RoyaltyModule__IpIsTagged.selector); + royaltyModule.payRoyaltyOnBehalf(ipAddr, ipAccount1, address(USDC), 100); + + vm.expectRevert(Errors.RoyaltyModule__IpIsTagged.selector); + royaltyModule.payRoyaltyOnBehalf(ipAccount1, ipAddr, address(USDC), 100); + } + function test_RoyaltyModule_payRoyaltyOnBehalf_revert_NotWhitelistedRoyaltyPolicy() public { uint256 royaltyAmount = 100 * 10 ** 6; address receiverIpId = address(7); @@ -487,6 +569,23 @@ contract TestRoyaltyModule is BaseTest { assertEq(ipRoyaltyVaultUSDCBalAfter - ipRoyaltyVaultUSDCBalBefore, royaltyAmount); } + function test_RoyaltyModule_payLicenseMintingFee_revert_IpIsTagged() public { + // raise dispute + vm.startPrank(ipAccount1); + USDC.approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(ipAddr, string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); + + // set dispute judgement + vm.startPrank(arbitrationRelayer); + disputeModule.setDisputeJudgement(1, true, ""); + + vm.startPrank(address(licensingModule)); + + vm.expectRevert(Errors.RoyaltyModule__IpIsTagged.selector); + royaltyModule.payLicenseMintingFee(ipAddr, ipAccount1, address(royaltyPolicyLAP), address(USDC), 100); + } + function test_RoyaltyModule_payLicenseMintingFee() public { uint256 royaltyAmount = 100 * 10 ** 6; address receiverIpId = address(7);