diff --git a/Makefile b/Makefile index 20114d0bb..3361a47fc 100644 --- a/Makefile +++ b/Makefile @@ -44,8 +44,8 @@ format: coverage: mkdir -p coverage forge coverage --report lcov - lcov --remove lcov.info -o coverage/lcov.info 'test/*' 'script/*' --rc branch_coverage=1 - genhtml coverage/lcov.info -o coverage --rc branch_coverage=1 --ignore-errors category + lcov --remove lcov.info -o coverage/lcov.info 'test/*' 'script/*' --rc lcov_branch_coverage=1 + genhtml coverage/lcov.info -o coverage --rc lcov_branch_coverage=1 abi: rm -rf abi diff --git a/contracts/interfaces/modules/dispute/IDisputeModule.sol b/contracts/interfaces/modules/dispute/IDisputeModule.sol index 60f0d31b4..3d4bab0c7 100644 --- a/contracts/interfaces/modules/dispute/IDisputeModule.sol +++ b/contracts/interfaces/modules/dispute/IDisputeModule.sol @@ -10,6 +10,7 @@ interface IDisputeModule { /// @param linkToDisputeEvidence The link of the dispute evidence /// @param targetTag The target tag of the dispute /// @param currentTag The current tag of the dispute + /// @param parentDisputeId The parent dispute id struct Dispute { address targetIpId; address disputeInitiator; @@ -17,6 +18,7 @@ interface IDisputeModule { bytes32 linkToDisputeEvidence; bytes32 targetTag; bytes32 currentTag; + uint256 parentDisputeId; } /// @notice Event emitted when a dispute tag whitelist status is updated @@ -73,6 +75,18 @@ interface IDisputeModule { /// @param data Custom data adjusted to each policy event DisputeCancelled(uint256 disputeId, bytes data); + /// @notice Event emitted when a derivative is tagged on a parent infringement + /// @param parentIpId The parent ipId which infringed + /// @param derivativeIpId The derivative ipId which was tagged + /// @param parentDisputeId The parent dispute id in which infringement was found + /// @param tag The tag of the dispute applied to the derivative + event DerivativeTaggedOnParentInfringement( + address parentIpId, + address derivativeIpId, + uint256 parentDisputeId, + bytes32 tag + ); + /// @notice Event emitted when a dispute is resolved /// @param disputeId The dispute id event DisputeResolved(uint256 disputeId); @@ -94,6 +108,7 @@ interface IDisputeModule { /// @return linkToDisputeEvidence The link of the dispute summary /// @return targetTag The target tag of the dispute /// @return currentTag The current tag of the dispute + /// @return parentDisputeId The parent dispute id function disputes( uint256 disputeId ) @@ -105,7 +120,8 @@ interface IDisputeModule { address arbitrationPolicy, bytes32 linkToDisputeEvidence, bytes32 targetTag, - bytes32 currentTag + bytes32 currentTag, + uint256 parentDisputeId ); /// @notice Indicates if a dispute tag is whitelisted @@ -161,7 +177,7 @@ interface IDisputeModule { /// @param targetIpId The ipId that is the target of the dispute /// @param linkToDisputeEvidence The link of the dispute evidence /// @param targetTag The target tag of the dispute - /// @param data The data to initialize the policy + /// @param data The data to raise a dispute /// @return disputeId The id of the newly raised dispute function raiseDispute( address targetIpId, @@ -181,9 +197,20 @@ interface IDisputeModule { /// @param data The data to cancel the dispute function cancelDispute(uint256 disputeId, bytes calldata data) external; + /// @notice Tags a derivative if a parent has been tagged with an infringement tag + /// @param parentIpId The infringing parent ipId + /// @param derivativeIpId The derivative ipId + /// @param parentDisputeId The dispute id that tagged the parent ipId as infringing + function tagDerivativeIfParentInfringed( + address parentIpId, + address derivativeIpId, + uint256 parentDisputeId + ) external; + /// @notice Resolves a dispute after it has been judged - /// @param disputeId The dispute id - function resolveDispute(uint256 disputeId) external; + /// @param disputeId The dispute + /// @param data The data to resolve the dispute + function resolveDispute(uint256 disputeId, bytes calldata data) external; /// @notice Returns true if the ipId is tagged with any tag (meaning at least one dispute went through) /// @param ipId The ipId diff --git a/contracts/interfaces/modules/dispute/policies/IArbitrationPolicy.sol b/contracts/interfaces/modules/dispute/policies/IArbitrationPolicy.sol index 4850ffe6c..fc067d933 100644 --- a/contracts/interfaces/modules/dispute/policies/IArbitrationPolicy.sol +++ b/contracts/interfaces/modules/dispute/policies/IArbitrationPolicy.sol @@ -36,6 +36,13 @@ interface IArbitrationPolicy { /// @param data The arbitrary data used to cancel the dispute function onDisputeCancel(address caller, uint256 disputeId, bytes calldata data) external; + /// @notice Executes custom logic on resolving dispute + /// @dev Enforced to be only callable by the DisputeModule + /// @param caller Address of the caller + /// @param disputeId The dispute id + /// @param data The arbitrary data used to resolve the dispute + function onResolveDispute(address caller, uint256 disputeId, bytes calldata data) external; + /// @notice Allows governance address to withdraw /// @dev Enforced to be only callable by the governance protocol admin. function governanceWithdraw() external; diff --git a/contracts/interfaces/registries/ILicenseRegistry.sol b/contracts/interfaces/registries/ILicenseRegistry.sol index 238fba368..248d38dc2 100644 --- a/contracts/interfaces/registries/ILicenseRegistry.sol +++ b/contracts/interfaces/registries/ILicenseRegistry.sol @@ -134,6 +134,12 @@ interface ILicenseRegistry { /// @return parentIpId The address of the parent IP. function getParentIp(address childIpId, uint256 index) external view returns (address parentIpId); + /// @notice Checks if an IP is a parent IP. + /// @param parentIpId The address of the parent IP. + /// @param childIpId The address of the child IP. + /// @return Whether the IP is a parent IP. + function isParentIp(address parentIpId, address childIpId) external view returns (bool); + /// @notice Gets the count of parent IPs. /// @param childIpId The address of the childIP. /// @return The count o parent IPs. diff --git a/contracts/lib/ArrayUtils.sol b/contracts/lib/ArrayUtils.sol index 29c5dec08..dbec7667d 100644 --- a/contracts/lib/ArrayUtils.sol +++ b/contracts/lib/ArrayUtils.sol @@ -5,12 +5,12 @@ pragma solidity 0.8.23; /// @notice Library for address array operations library ArrayUtils { /// @notice Finds the index of the first occurrence of the given element. - /// @param _array The input array to search - /// @param _element The value to find + /// @param array The input array to search + /// @param element The value to find /// @return Returns (index and isIn) for the first occurrence starting from index 0 - function indexOf(address[] memory _array, address _element) internal pure returns (uint32, bool) { - for (uint32 i = 0; i < _array.length; i++) { - if (_array[i] == _element) return (i, true); + function indexOf(address[] memory array, address element) internal pure returns (uint32, bool) { + for (uint32 i = 0; i < array.length; i++) { + if (array[i] == element) return (i, true); } return (0, false); } diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 36c5fb981..dd8efdf52 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -233,10 +233,19 @@ library Errors { error DisputeModule__NotInDisputeState(); error DisputeModule__NotAbleToResolve(); error DisputeModule__NotRegisteredIpId(); + error DisputeModule__ParentIpIdMismatch(); + error DisputeModule__ParentNotTagged(); + error DisputeModule__NotDerivative(); + error DisputeModule__ParentDisputeNotResolved(); + error DisputeModule__ZeroLicenseRegistry(); + error DisputeModule__ZeroAssetRegistry(); + error DisputeModule__ZeroController(); + error DisputeModule__ZeroAccessManager(); error ArbitrationPolicySP__ZeroDisputeModule(); error ArbitrationPolicySP__ZeroPaymentToken(); error ArbitrationPolicySP__NotDisputeModule(); + error ArbitrationPolicySP__ZeroAccessManager(); //////////////////////////////////////////////////////////////////////////// // Royalty Module // @@ -254,6 +263,7 @@ library Errors { error RoyaltyModule__NoParentsOnLinking(); error RoyaltyModule__ZeroDisputeModule(); error RoyaltyModule__IpIsTagged(); + error RoyaltyModule__ZeroAccessManager(); error RoyaltyPolicyLAP__ZeroRoyaltyModule(); error RoyaltyPolicyLAP__ZeroLiquidSplitFactory(); @@ -270,8 +280,8 @@ library Errors { error RoyaltyPolicyLAP__UnlinkableToParents(); error RoyaltyPolicyLAP__LastPositionNotAbleToMintLicense(); error RoyaltyPolicyLAP__ZeroIpRoyaltyVaultBeacon(); + error RoyaltyPolicyLAP__ZeroAccessManager(); - error IpRoyaltyVault__ZeroIpId(); error IpRoyaltyVault__ZeroRoyaltyPolicyLAP(); error IpRoyaltyVault__NotRoyaltyPolicyLAP(); error IpRoyaltyVault__SnapshotIntervalTooShort(); diff --git a/contracts/modules/dispute/DisputeModule.sol b/contracts/modules/dispute/DisputeModule.sol index feb22ae1d..f4bc1ddba 100644 --- a/contracts/modules/dispute/DisputeModule.sol +++ b/contracts/modules/dispute/DisputeModule.sol @@ -6,11 +6,13 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; // solhint-disable-next-line max-line-length import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; +import { MulticallUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; import { DISPUTE_MODULE_KEY } from "../../lib/modules/Module.sol"; import { BaseModule } from "../../modules/BaseModule.sol"; import { AccessControlled } from "../../access/AccessControlled.sol"; import { IIPAssetRegistry } from "../../interfaces/registries/IIPAssetRegistry.sol"; +import { ILicenseRegistry } from "../../interfaces/registries/ILicenseRegistry.sol"; import { IDisputeModule } from "../../interfaces/modules/dispute/IDisputeModule.sol"; import { IArbitrationPolicy } from "../../interfaces/modules/dispute/policies/IArbitrationPolicy.sol"; import { Errors } from "../../lib/Errors.sol"; @@ -25,7 +27,8 @@ contract DisputeModule is AccessManagedUpgradeable, ReentrancyGuardUpgradeable, AccessControlled, - UUPSUpgradeable + UUPSUpgradeable, + MulticallUpgradeable { using EnumerableSet for EnumerableSet.Bytes32Set; @@ -64,21 +67,38 @@ contract DisputeModule is /// @custom:oz-upgrades-unsafe-allow state-variable-immutable IIPAssetRegistry public immutable IP_ASSET_REGISTRY; + /// @notice Protocol-wide license registry + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + ILicenseRegistry public immutable LICENSE_REGISTRY; + /// Constructor /// @param controller The address of the access controller /// @param assetRegistry The address of the asset registry + /// @param licenseRegistry The address of the license registry /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address controller, address assetRegistry) AccessControlled(controller, assetRegistry) { + constructor( + address controller, + address assetRegistry, + address licenseRegistry + ) AccessControlled(controller, assetRegistry) { + if (licenseRegistry == address(0)) revert Errors.DisputeModule__ZeroLicenseRegistry(); + if (assetRegistry == address(0)) revert Errors.DisputeModule__ZeroAssetRegistry(); + if (controller == address(0)) revert Errors.DisputeModule__ZeroController(); + IP_ASSET_REGISTRY = IIPAssetRegistry(assetRegistry); + LICENSE_REGISTRY = ILicenseRegistry(licenseRegistry); _disableInitializers(); } - /// @notice initializer for this implementation contract + /// @notice Initializer for this implementation contract /// @param accessManager The address of the protocol admin roles contract function initialize(address accessManager) external initializer { + if (accessManager == address(0)) revert Errors.DisputeModule__ZeroAccessManager(); + __AccessManaged_init(accessManager); __ReentrancyGuard_init(); __UUPSUpgradeable_init(); + __Multicall_init(); } /// @notice Whitelists a dispute tag @@ -178,7 +198,8 @@ contract DisputeModule is arbitrationPolicy: arbitrationPolicy, linkToDisputeEvidence: linkToDisputeEvidenceBytes, targetTag: targetTag, - currentTag: IN_DISPUTE + currentTag: IN_DISPUTE, + parentDisputeId: 0 }); IArbitrationPolicy(arbitrationPolicy).onRaiseDispute(msg.sender, data); @@ -239,19 +260,76 @@ contract DisputeModule is emit DisputeCancelled(disputeId, data); } + /// @notice Tags a derivative if a parent has been tagged with an infringement tag + /// @param parentIpId The infringing parent ipId + /// @param derivativeIpId The derivative ipId + /// @param parentDisputeId The dispute id that tagged the parent ipId as infringing + function tagDerivativeIfParentInfringed( + address parentIpId, + address derivativeIpId, + uint256 parentDisputeId + ) external { + DisputeModuleStorage storage $ = _getDisputeModuleStorage(); + + Dispute memory parentDispute = $.disputes[parentDisputeId]; + if (parentDispute.targetIpId != parentIpId) revert Errors.DisputeModule__ParentIpIdMismatch(); + + // a dispute current tag prior to being resolved can be in 3 states - IN_DISPUTE, 0, or a tag (ie. "PLAGIARISM) + // by restricting IN_DISPUTE and 0 - it is ensire the parent has been tagged before resolving dispute + if (parentDispute.currentTag == IN_DISPUTE || parentDispute.currentTag == bytes32(0)) + revert Errors.DisputeModule__ParentNotTagged(); + + if (!LICENSE_REGISTRY.isParentIp(parentIpId, derivativeIpId)) revert Errors.DisputeModule__NotDerivative(); + + address arbitrationPolicy = $.arbitrationPolicies[derivativeIpId]; + if (!$.isWhitelistedArbitrationPolicy[arbitrationPolicy]) arbitrationPolicy = $.baseArbitrationPolicy; + + uint256 disputeId = ++$.disputeCounter; + + $.disputes[disputeId] = Dispute({ + targetIpId: derivativeIpId, + disputeInitiator: msg.sender, + arbitrationPolicy: arbitrationPolicy, + linkToDisputeEvidence: "", + targetTag: parentDispute.currentTag, + currentTag: parentDispute.currentTag, + parentDisputeId: parentDisputeId + }); + + $.successfulDisputesPerIp[derivativeIpId]++; + + emit DerivativeTaggedOnParentInfringement( + parentIpId, + derivativeIpId, + parentDisputeId, + parentDispute.currentTag + ); + } + /// @notice Resolves a dispute after it has been judged /// @param disputeId The dispute id - function resolveDispute(uint256 disputeId) external { + /// @param data The data to resolve the dispute + function resolveDispute(uint256 disputeId, bytes calldata data) external { DisputeModuleStorage storage $ = _getDisputeModuleStorage(); Dispute memory dispute = $.disputes[disputeId]; - if (msg.sender != dispute.disputeInitiator) revert Errors.DisputeModule__NotDisputeInitiator(); + // there are two types of disputes - those that are subject to judgment and those that are not + // the way to distinguish is by whether dispute.parentDisputeId is 0 or higher than 0 + // for the former - only the dispute initiator can resolve + if (dispute.parentDisputeId == 0 && msg.sender != dispute.disputeInitiator) + revert Errors.DisputeModule__NotDisputeInitiator(); + // for the latter - resolving is permissionless as long as the parent dispute has been resolved + if (dispute.parentDisputeId > 0 && $.disputes[dispute.parentDisputeId].currentTag != bytes32(0)) + revert Errors.DisputeModule__ParentDisputeNotResolved(); + if (dispute.currentTag == IN_DISPUTE || dispute.currentTag == bytes32(0)) revert Errors.DisputeModule__NotAbleToResolve(); $.successfulDisputesPerIp[dispute.targetIpId]--; $.disputes[disputeId].currentTag = bytes32(0); + IArbitrationPolicy(dispute.arbitrationPolicy).onResolveDispute(msg.sender, disputeId, data); + emit DisputeResolved(disputeId); } @@ -283,6 +361,7 @@ contract DisputeModule is /// @return linkToDisputeEvidence The link of the dispute summary /// @return targetTag The target tag of the dispute /// @return currentTag The current tag of the dispute + /// @return parentDisputeId The parent dispute id function disputes( uint256 disputeId ) @@ -294,17 +373,19 @@ contract DisputeModule is address arbitrationPolicy, bytes32 linkToDisputeEvidence, bytes32 targetTag, - bytes32 currentTag + bytes32 currentTag, + uint256 parentDisputeId ) { - DisputeModuleStorage storage $ = _getDisputeModuleStorage(); + Dispute memory dispute = _getDisputeModuleStorage().disputes[disputeId]; return ( - $.disputes[disputeId].targetIpId, - $.disputes[disputeId].disputeInitiator, - $.disputes[disputeId].arbitrationPolicy, - $.disputes[disputeId].linkToDisputeEvidence, - $.disputes[disputeId].targetTag, - $.disputes[disputeId].currentTag + dispute.targetIpId, + dispute.disputeInitiator, + dispute.arbitrationPolicy, + dispute.linkToDisputeEvidence, + dispute.targetTag, + dispute.currentTag, + dispute.parentDisputeId ); } diff --git a/contracts/modules/dispute/policies/ArbitrationPolicySP.sol b/contracts/modules/dispute/policies/ArbitrationPolicySP.sol index e4db914c3..666f4c8f7 100644 --- a/contracts/modules/dispute/policies/ArbitrationPolicySP.sol +++ b/contracts/modules/dispute/policies/ArbitrationPolicySP.sol @@ -34,28 +34,32 @@ contract ArbitrationPolicySP is IArbitrationPolicy, AccessManagedUpgradeable, UU } /// Constructor - /// @param _disputeModule The dispute module address - /// @param _paymentToken The ERC20 payment token address - /// @param _arbitrationPrice The arbitration price + /// @param disputeModule The dispute module address + /// @param paymentToken The ERC20 payment token address + /// @param arbitrationPrice The arbitration price /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address _disputeModule, address _paymentToken, uint256 _arbitrationPrice) { - if (_disputeModule == address(0)) revert Errors.ArbitrationPolicySP__ZeroDisputeModule(); - if (_paymentToken == address(0)) revert Errors.ArbitrationPolicySP__ZeroPaymentToken(); + constructor(address disputeModule, address paymentToken, uint256 arbitrationPrice) { + if (disputeModule == address(0)) revert Errors.ArbitrationPolicySP__ZeroDisputeModule(); + if (paymentToken == address(0)) revert Errors.ArbitrationPolicySP__ZeroPaymentToken(); - DISPUTE_MODULE = _disputeModule; - PAYMENT_TOKEN = _paymentToken; - ARBITRATION_PRICE = _arbitrationPrice; + DISPUTE_MODULE = disputeModule; + PAYMENT_TOKEN = paymentToken; + ARBITRATION_PRICE = arbitrationPrice; + + _disableInitializers(); } /// @notice initializer for this implementation contract /// @param accessManager The address of the protocol admin roles contract function initialize(address accessManager) public initializer { + if (accessManager == address(0)) revert Errors.ArbitrationPolicySP__ZeroAccessManager(); + __AccessManaged_init(accessManager); __UUPSUpgradeable_init(); } - /// @notice Executes custom logic on raising dispute. - /// @dev Enforced to be only callable by the DisputeModule. + /// @notice Executes custom logic on raising dispute + /// @dev Enforced to be only callable by the DisputeModule /// @param caller Address of the caller /// @param data The arbitrary data used to raise the dispute function onRaiseDispute(address caller, bytes calldata data) external onlyDisputeModule { @@ -63,27 +67,34 @@ contract ArbitrationPolicySP is IArbitrationPolicy, AccessManagedUpgradeable, UU IERC20(PAYMENT_TOKEN).safeTransferFrom(caller, address(this), ARBITRATION_PRICE); } - /// @notice Executes custom logic on disputing judgement. - /// @dev Enforced to be only callable by the DisputeModule. + /// @notice Executes custom logic on disputing judgement + /// @dev Enforced to be only callable by the DisputeModule /// @param disputeId The dispute id /// @param decision The decision of the dispute /// @param data The arbitrary data used to set the dispute judgement function onDisputeJudgement(uint256 disputeId, bool decision, bytes calldata data) external onlyDisputeModule { if (decision) { - (, address disputeInitiator, , , , ) = IDisputeModule(DISPUTE_MODULE).disputes(disputeId); + (, address disputeInitiator, , , , , ) = IDisputeModule(DISPUTE_MODULE).disputes(disputeId); IERC20(PAYMENT_TOKEN).safeTransfer(disputeInitiator, ARBITRATION_PRICE); } } - /// @notice Executes custom logic on disputing cancel. - /// @dev Enforced to be only callable by the DisputeModule. + /// @notice Executes custom logic on disputing cancel + /// @dev Enforced to be only callable by the DisputeModule /// @param caller Address of the caller /// @param disputeId The dispute id /// @param data The arbitrary data used to cancel the dispute function onDisputeCancel(address caller, uint256 disputeId, bytes calldata data) external onlyDisputeModule {} + /// @notice Executes custom logic on resolving dispute + /// @dev Enforced to be only callable by the DisputeModule + /// @param caller Address of the caller + /// @param disputeId The dispute id + /// @param data The arbitrary data used to resolve the dispute + function onResolveDispute(address caller, uint256 disputeId, bytes calldata data) external onlyDisputeModule {} + /// @notice Allows governance address to withdraw - /// @dev Enforced to be only callable by the governance protocol admin. + /// @dev Enforced to be only callable by the governance protocol admin function governanceWithdraw() external restricted { uint256 balance = IERC20(PAYMENT_TOKEN).balanceOf(address(this)); IERC20(PAYMENT_TOKEN).safeTransfer(msg.sender, balance); diff --git a/contracts/modules/royalty/RoyaltyModule.sol b/contracts/modules/royalty/RoyaltyModule.sol index cd5f9416e..073173e51 100644 --- a/contracts/modules/royalty/RoyaltyModule.sol +++ b/contracts/modules/royalty/RoyaltyModule.sol @@ -55,9 +55,11 @@ contract RoyaltyModule is _disableInitializers(); } - /// @notice initializer for this implementation contract + /// @notice Initializer for this implementation contract /// @param accessManager The address of the protocol admin roles contract function initialize(address accessManager) external initializer { + if (accessManager == address(0)) revert Errors.RoyaltyModule__ZeroAccessManager(); + __AccessManaged_init(accessManager); __ReentrancyGuard_init(); __UUPSUpgradeable_init(); diff --git a/contracts/modules/royalty/policies/IpRoyaltyVault.sol b/contracts/modules/royalty/policies/IpRoyaltyVault.sol index d8f57977f..711755506 100644 --- a/contracts/modules/royalty/policies/IpRoyaltyVault.sol +++ b/contracts/modules/royalty/policies/IpRoyaltyVault.sol @@ -86,8 +86,6 @@ contract IpRoyaltyVault is IIpRoyaltyVault, ERC20SnapshotUpgradeable, Reentrancy uint32 unclaimedTokens, address ipIdAddress ) external initializer { - if (ipIdAddress == address(0)) revert Errors.IpRoyaltyVault__ZeroIpId(); - IpRoyaltyVaultStorage storage $ = _getIpRoyaltyVaultStorage(); $.ipId = ipIdAddress; diff --git a/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol b/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol index 55cf17c3d..4c9ad7fc8 100644 --- a/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol +++ b/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol @@ -72,9 +72,11 @@ contract RoyaltyPolicyLAP is IRoyaltyPolicyLAP, AccessManagedUpgradeable, Reentr } /// @notice Initializer for this implementation contract - /// @param governance The governance address - function initialize(address governance) external initializer { - __AccessManaged_init(governance); + /// @param accessManager The address of the protocol admin roles contract + function initialize(address accessManager) external initializer { + if (accessManager == address(0)) revert Errors.RoyaltyPolicyLAP__ZeroAccessManager(); + + __AccessManaged_init(accessManager); __ReentrancyGuard_init(); __UUPSUpgradeable_init(); } diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index 9aef69618..2701570f0 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -346,7 +346,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr return _getLicenseRegistryStorage().attachedLicenseTerms[ipId].length(); } - /// @notice got the derivative IP of an IP by its index. + /// @notice Gets the derivative IP of an IP by its index. /// @param parentIpId The address of the IP. /// @param index The index of the derivative IP within the array of all derivative IPs of the IP. /// @return childIpId The address of the derivative IP. @@ -377,6 +377,10 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr parentIpId = $.parentIps[childIpId].at(index); } + function isParentIp(address parentIpId, address childIpId) external view returns (bool) { + return _getLicenseRegistryStorage().parentIps[childIpId].contains(parentIpId); + } + /// @notice Gets the count of parent IPs. /// @param childIpId The address of the childIP. /// @return The count o parent IPs. diff --git a/script/foundry/utils/DeployHelper.sol b/script/foundry/utils/DeployHelper.sol index 2805471a2..83a6eee82 100644 --- a/script/foundry/utils/DeployHelper.sol +++ b/script/foundry/utils/DeployHelper.sol @@ -215,18 +215,6 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag impl = address(0); _postdeploy(contractKey, address(royaltyModule)); - contractKey = "DisputeModule"; - _predeploy(contractKey); - impl = address(new DisputeModule(address(accessController), address(ipAssetRegistry))); - disputeModule = DisputeModule( - TestProxyHelper.deployUUPSProxy( - impl, - abi.encodeCall(DisputeModule.initialize, address(protocolAccessManager)) - ) - ); - impl = address(0); - _postdeploy(contractKey, address(disputeModule)); - contractKey = "LicenseRegistry"; _predeploy(contractKey); impl = address(new LicenseRegistry()); @@ -239,6 +227,18 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag impl = address(0); // Make sure we don't deploy wrong impl _postdeploy(contractKey, address(licenseRegistry)); + contractKey = "DisputeModule"; + _predeploy(contractKey); + impl = address(new DisputeModule(address(accessController), address(ipAssetRegistry), address(licenseRegistry))); + disputeModule = DisputeModule( + TestProxyHelper.deployUUPSProxy( + impl, + abi.encodeCall(DisputeModule.initialize, address(protocolAccessManager)) + ) + ); + impl = address(0); + _postdeploy(contractKey, address(disputeModule)); + contractKey = "LicenseToken"; _predeploy(contractKey); impl = address(new LicenseToken()); diff --git a/test/foundry/integration/flows/disputes/Disputes.t.sol b/test/foundry/integration/flows/disputes/Disputes.t.sol index 271844752..e75638fb7 100644 --- a/test/foundry/integration/flows/disputes/Disputes.t.sol +++ b/test/foundry/integration/flows/disputes/Disputes.t.sol @@ -116,7 +116,7 @@ contract Flows_Integration_Disputes is BaseIntegration { uint256 disputeId = _disputeIp(u.bob, ipAcct[1]); vm.prank(u.bob); - disputeModule.resolveDispute(disputeId); + disputeModule.resolveDispute(disputeId, ""); assertEq(licenseToken.balanceOf(u.carl), 0); diff --git a/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol b/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol index 2746c02a3..988fc96d4 100644 --- a/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol +++ b/test/foundry/integration/flows/licensing/LicensingIntegration.t.sol @@ -119,7 +119,10 @@ contract e2e is Test { ); vm.label(address(royaltyModule), "RoyaltyModule"); - impl = address(new DisputeModule(address(accessController), address(ipAssetRegistry))); + impl = address( + new DisputeModule(address(accessController), address(ipAssetRegistry), address(licenseRegistry)) + ); + disputeModule = DisputeModule( TestProxyHelper.deployUUPSProxy( impl, diff --git a/test/foundry/modules/dispute/ArbitrationPolicySP.t.sol b/test/foundry/modules/dispute/ArbitrationPolicySP.t.sol index 596cd980d..62957f02f 100644 --- a/test/foundry/modules/dispute/ArbitrationPolicySP.t.sol +++ b/test/foundry/modules/dispute/ArbitrationPolicySP.t.sol @@ -9,6 +9,7 @@ import { Errors } from "contracts/lib/Errors.sol"; import { ArbitrationPolicySP } from "contracts/modules/dispute/policies/ArbitrationPolicySP.sol"; // test import { BaseTest } from "test/foundry/utils/BaseTest.t.sol"; +import { TestProxyHelper } from "test/foundry/utils/TestProxyHelper.sol"; contract TestArbitrationPolicySP is BaseTest { event GovernanceWithdrew(uint256 amount); @@ -96,6 +97,19 @@ contract TestArbitrationPolicySP is BaseTest { assertEq(arbitrationPolicySP.ARBITRATION_PRICE(), arbitrationPrice); } + function test_ArbitrationPolicySP_revert_ZeroAccessManager() public { + address disputeModule = address(1); + address paymentToken = address(2); + uint256 arbitrationPrice = 1000; + + address impl = address(new ArbitrationPolicySP(address(disputeModule), paymentToken, arbitrationPrice)); + + vm.expectRevert(Errors.ArbitrationPolicySP__ZeroAccessManager.selector); + arbitrationPolicySP = ArbitrationPolicySP( + TestProxyHelper.deployUUPSProxy(impl, abi.encodeCall(ArbitrationPolicySP.initialize, address(0))) + ); + } + function test_ArbitrationPolicySP_onRaiseDispute_NotDisputeModule() public { vm.expectRevert(Errors.ArbitrationPolicySP__NotDisputeModule.selector); arbitrationPolicySP.onRaiseDispute(address(1), new bytes(0)); diff --git a/test/foundry/modules/dispute/DisputeModule.t.sol b/test/foundry/modules/dispute/DisputeModule.t.sol index b1df27c79..ac964c60f 100644 --- a/test/foundry/modules/dispute/DisputeModule.t.sol +++ b/test/foundry/modules/dispute/DisputeModule.t.sol @@ -9,6 +9,7 @@ import { Errors } from "contracts/lib/Errors.sol"; import { IModule } from "contracts/interfaces/modules/base/IModule.sol"; import { ArbitrationPolicySP } from "contracts/modules/dispute/policies/ArbitrationPolicySP.sol"; import { ShortStringOps } from "contracts/utils/ShortStringOps.sol"; +import { IDisputeModule } from "contracts/interfaces/modules/dispute/IDisputeModule.sol"; // test import { BaseTest } from "test/foundry/utils/BaseTest.t.sol"; import { TestProxyHelper } from "test/foundry/utils/TestProxyHelper.sol"; @@ -36,6 +37,7 @@ contract DisputeModuleTest is BaseTest { address internal ipAccount2 = address(0x111000bbb); address internal ipAddr; + address internal ipAddr2; address internal arbitrationRelayer; ArbitrationPolicySP internal arbitrationPolicySP2; @@ -70,6 +72,7 @@ contract DisputeModuleTest is BaseTest { }); mockNFT.mintId(u.alice, 0); + mockNFT.mintId(u.bob, 1); address expectedAddr = ERC6551AccountLib.computeAddress( address(erc6551Registry), @@ -79,17 +82,44 @@ contract DisputeModuleTest is BaseTest { address(mockNFT), 0 ); - vm.label(expectedAddr, "IPAccount0"); vm.startPrank(u.alice); ipAddr = ipAssetRegistry.register(address(mockNFT), 0); - licensingModule.attachLicenseTerms(ipAddr, address(pilTemplate), getSelectedPILicenseTermsId("cheap_flexible")); + // Bob mints 1 license of policy "pil-commercial-remix" from IPAccount1 and registers the derivative IP for + // NFT tokenId 2. + vm.startPrank(u.bob); + + uint256 mintAmount = 3; + erc20.approve(address(royaltyPolicyLAP), type(uint256).max); + + uint256[] memory licenseIds = new uint256[](1); + + licenseIds[0] = licensingModule.mintLicenseTokens({ + licensorIpId: ipAddr, + licenseTemplate: address(pilTemplate), + licenseTermsId: getSelectedPILicenseTermsId("cheap_flexible"), + amount: mintAmount, + receiver: u.bob, + royaltyContext: "" + }); // first license minted + + ipAddr2 = ipAssetRegistry.register(address(mockNFT), 1); + + licensingModule.registerDerivativeWithLicenseTokens(ipAddr2, licenseIds, ""); + + vm.stopPrank(); + // set arbitration policy vm.startPrank(ipAddr); disputeModule.setArbitrationPolicy(ipAddr, address(arbitrationPolicySP)); vm.stopPrank(); + + // set arbitration policy + vm.startPrank(ipAddr2); + disputeModule.setArbitrationPolicy(ipAddr2, address(arbitrationPolicySP)); + vm.stopPrank(); } function test_DisputeModule_whitelistDisputeTag_revert_ZeroDisputeTag() public { @@ -185,7 +215,7 @@ contract DisputeModuleTest is BaseTest { disputeModule.setArbitrationPolicy(ipAddr, address(arbitrationPolicySP2)); } - function test_setArbitrationPolicy() public { + function test_DisputeModule_setArbitrationPolicy() public { vm.startPrank(u.admin); disputeModule.whitelistArbitrationPolicy(address(arbitrationPolicySP2), true); vm.stopPrank(); @@ -199,22 +229,22 @@ contract DisputeModuleTest is BaseTest { assertEq(disputeModule.arbitrationPolicies(ipAddr), address(arbitrationPolicySP2)); } - function test_DisputeModule_PolicySP_raiseDispute_revert_NotRegisteredIpId() public { + function test_DisputeModule_raiseDispute_revert_NotRegisteredIpId() public { vm.expectRevert(Errors.DisputeModule__NotRegisteredIpId.selector); disputeModule.raiseDispute(address(1), string("urlExample"), "PLAGIARISM", ""); } - function test_DisputeModule_PolicySP_raiseDispute_revert_NotWhitelistedDisputeTag() public { + function test_DisputeModule_raiseDispute_revert_NotWhitelistedDisputeTag() public { vm.expectRevert(Errors.DisputeModule__NotWhitelistedDisputeTag.selector); disputeModule.raiseDispute(ipAddr, string("urlExample"), "NOT_WHITELISTED", ""); } - function test_DisputeModule_PolicySP_raiseDispute_revert_ZeroLinkToDisputeEvidence() public { + function test_DisputeModule_raiseDispute_revert_ZeroLinkToDisputeEvidence() public { vm.expectRevert(Errors.DisputeModule__ZeroLinkToDisputeEvidence.selector); disputeModule.raiseDispute(ipAddr, string(""), "PLAGIARISM", ""); } - function test_DisputeModule_PolicySP_raiseDispute_BlacklistedPolicy() public { + function test_DisputeModule_raiseDispute_BlacklistedPolicy() public { vm.startPrank(u.admin); disputeModule.whitelistArbitrationPolicy(address(arbitrationPolicySP), false); vm.stopPrank(); @@ -249,9 +279,11 @@ contract DisputeModuleTest is BaseTest { address arbitrationPolicy, bytes32 linkToDisputeEvidence, bytes32 targetTag, - bytes32 currentTag + bytes32 currentTag, + uint256 parentDisputeId ) = disputeModule.disputes(disputeIdAfter); + assertEq(disputeIdAfter, 1); assertEq(disputeIdAfter - disputeIdBefore, 1); assertEq(ipAccount1USDCBalanceBefore - ipAccount1USDCBalanceAfter, ARBITRATION_PRICE); assertEq(arbitrationPolicySPUSDCBalanceAfter - arbitrationPolicySPUSDCBalanceBefore, ARBITRATION_PRICE); @@ -261,9 +293,10 @@ contract DisputeModuleTest is BaseTest { assertEq(linkToDisputeEvidence, ShortStringOps.stringToBytes32("urlExample")); assertEq(targetTag, bytes32("PLAGIARISM")); assertEq(currentTag, bytes32("IN_DISPUTE")); + assertEq(parentDisputeId, 0); } - function test_DisputeModule_PolicySP_raiseDispute() public { + function test_DisputeModule_raiseDispute() public { vm.startPrank(ipAccount1); IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); @@ -294,7 +327,8 @@ contract DisputeModuleTest is BaseTest { address arbitrationPolicy, bytes32 linkToDisputeEvidence, bytes32 targetTag, - bytes32 currentTag + bytes32 currentTag, + uint256 parentDisputeId ) = disputeModule.disputes(disputeIdAfter); assertEq(disputeIdAfter - disputeIdBefore, 1); @@ -306,14 +340,15 @@ contract DisputeModuleTest is BaseTest { assertEq(linkToDisputeEvidence, ShortStringOps.stringToBytes32("urlExample")); assertEq(targetTag, bytes32("PLAGIARISM")); assertEq(currentTag, bytes32("IN_DISPUTE")); + assertEq(parentDisputeId, 0); } - function test_DisputeModule_PolicySP_setDisputeJudgement_revert_NotInDisputeState() public { + function test_DisputeModule_setDisputeJudgement_revert_NotInDisputeState() public { vm.expectRevert(Errors.DisputeModule__NotInDisputeState.selector); disputeModule.setDisputeJudgement(1, true, ""); } - function test_DisputeModule_PolicySP_setDisputeJudgement_revert_NotWhitelistedArbitrationRelayer() public { + function test_DisputeModule_setDisputeJudgement_revert_NotWhitelistedArbitrationRelayer() public { // raise dispute vm.startPrank(ipAccount1); IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); @@ -324,7 +359,7 @@ contract DisputeModuleTest is BaseTest { disputeModule.setDisputeJudgement(1, true, ""); } - function test_DisputeModule_PolicySP_setDisputeJudgement_True() public { + function test_DisputeModule_setDisputeJudgement_True() public { // raise dispute vm.startPrank(ipAccount1); IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); @@ -332,7 +367,7 @@ contract DisputeModuleTest is BaseTest { vm.stopPrank(); // set dispute judgement - (, , , , , bytes32 currentTagBefore) = disputeModule.disputes(1); + (, , , , , bytes32 currentTagBefore, ) = disputeModule.disputes(1); uint256 ipAccount1USDCBalanceBefore = USDC.balanceOf(ipAccount1); uint256 arbitrationPolicySPUSDCBalanceBefore = USDC.balanceOf(address(arbitrationPolicySP)); @@ -342,7 +377,7 @@ contract DisputeModuleTest is BaseTest { vm.startPrank(arbitrationRelayer); disputeModule.setDisputeJudgement(1, true, ""); - (, , , , , bytes32 currentTagAfter) = disputeModule.disputes(1); + (, , , , , bytes32 currentTagAfter, ) = disputeModule.disputes(1); uint256 ipAccount1USDCBalanceAfter = USDC.balanceOf(ipAccount1); uint256 arbitrationPolicySPUSDCBalanceAfter = USDC.balanceOf(address(arbitrationPolicySP)); @@ -353,7 +388,7 @@ contract DisputeModuleTest is BaseTest { assertTrue(disputeModule.isIpTagged(ipAddr)); } - function test_DisputeModule_PolicySP_setDisputeJudgement_False() public { + function test_DisputeModule_setDisputeJudgement_False() public { // raise dispute vm.startPrank(ipAccount1); IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); @@ -361,7 +396,7 @@ contract DisputeModuleTest is BaseTest { vm.stopPrank(); // set dispute judgement - (, , , , , bytes32 currentTagBefore) = disputeModule.disputes(1); + (, , , , , bytes32 currentTagBefore, ) = disputeModule.disputes(1); uint256 ipAccount1USDCBalanceBefore = USDC.balanceOf(ipAccount1); uint256 arbitrationPolicySPUSDCBalanceBefore = USDC.balanceOf(address(arbitrationPolicySP)); @@ -371,7 +406,7 @@ contract DisputeModuleTest is BaseTest { vm.startPrank(arbitrationRelayer); disputeModule.setDisputeJudgement(1, false, ""); - (, , , , , bytes32 currentTagAfter) = disputeModule.disputes(1); + (, , , , , bytes32 currentTagAfter, ) = disputeModule.disputes(1); uint256 ipAccount1USDCBalanceAfter = USDC.balanceOf(ipAccount1); uint256 arbitrationPolicySPUSDCBalanceAfter = USDC.balanceOf(address(arbitrationPolicySP)); @@ -382,7 +417,7 @@ contract DisputeModuleTest is BaseTest { assertFalse(disputeModule.isIpTagged(ipAddr)); } - function test_DisputeModule_PolicySP_cancelDispute_revert_NotDisputeInitiator() public { + function test_DisputeModule_cancelDispute_revert_NotDisputeInitiator() public { // raise dispute vm.startPrank(ipAccount1); IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); @@ -393,19 +428,19 @@ contract DisputeModuleTest is BaseTest { disputeModule.cancelDispute(1, ""); } - function test_DisputeModule_PolicySP_cancelDispute_revert_NotInDisputeState() public { + function test_DisputeModule_cancelDispute_revert_NotInDisputeState() public { vm.expectRevert(Errors.DisputeModule__NotInDisputeState.selector); disputeModule.cancelDispute(1, ""); } - function test_DisputeModule_PolicySP_cancelDispute() public { + function test_DisputeModule_cancelDispute() public { // raise dispute vm.startPrank(ipAccount1); IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); disputeModule.raiseDispute(ipAddr, string("urlExample"), "PLAGIARISM", ""); vm.stopPrank(); - (, , , , , bytes32 currentTagBeforeCancel) = disputeModule.disputes(1); + (, , , , , bytes32 currentTagBeforeCancel, ) = disputeModule.disputes(1); vm.startPrank(ipAccount1); vm.expectEmit(true, true, true, true, address(disputeModule)); @@ -414,16 +449,95 @@ contract DisputeModuleTest is BaseTest { disputeModule.cancelDispute(1, ""); vm.stopPrank(); - (, , , , , bytes32 currentTagAfterCancel) = disputeModule.disputes(1); + (, , , , , bytes32 currentTagAfterCancel, ) = disputeModule.disputes(1); assertEq(currentTagBeforeCancel, bytes32("IN_DISPUTE")); assertEq(currentTagAfterCancel, bytes32(0)); assertFalse(disputeModule.isIpTagged(ipAddr)); } + function test_DisputeModule_tagDerivativeIfParentInfringed_revert_ParentIpIdMismatch() public { + vm.expectRevert(Errors.DisputeModule__ParentIpIdMismatch.selector); + disputeModule.tagDerivativeIfParentInfringed(address(1), address(2), 1); + } + + function test_DisputeModule_tagDerivativeIfParentInfringed_revert_ParentNotTagged() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(ipAddr, string("urlExample"), "PLAGIARISM", ""); + vm.stopPrank(); + + vm.expectRevert(Errors.DisputeModule__ParentNotTagged.selector); + disputeModule.tagDerivativeIfParentInfringed(ipAddr, ipAddr2, 1); + } + + function test_DisputeModule_tagDerivativeIfParentInfringed_revert_NotDerivative() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(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.stopPrank(); + + vm.expectRevert(Errors.DisputeModule__NotDerivative.selector); + disputeModule.tagDerivativeIfParentInfringed(ipAddr, address(0), 1); + } + + function test_DisputeModule_tagDerivativeIfParentInfringed() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(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.stopPrank(); + + assertEq(licenseRegistry.isParentIp(ipAddr, ipAddr2), true); + + // tag child ip + vm.startPrank(address(1)); + vm.expectEmit(true, true, true, true, address(disputeModule)); + emit IDisputeModule.DerivativeTaggedOnParentInfringement(ipAddr, ipAddr2, 1, "PLAGIARISM"); + + uint256 disputeIdBefore = disputeModule.disputeCounter(); + + disputeModule.tagDerivativeIfParentInfringed(ipAddr, ipAddr2, 1); + + uint256 disputeIdAfter = disputeModule.disputeCounter(); + + ( + address targetIpId, + address disputeInitiator, + address arbitrationPolicy, + bytes32 linkToDisputeEvidence, + bytes32 targetTag, + bytes32 currentTag, + uint256 parentDisputeId + ) = disputeModule.disputes(disputeIdAfter); + + assertEq(disputeIdAfter - disputeIdBefore, 1); + assertEq(disputeIdAfter, 2); + assertTrue(disputeModule.isIpTagged(ipAddr2)); + assertEq(targetIpId, ipAddr2); + assertEq(disputeInitiator, address(1)); + assertEq(arbitrationPolicy, address(arbitrationPolicySP)); + assertEq(linkToDisputeEvidence, bytes32(0)); + assertEq(targetTag, bytes32("PLAGIARISM")); + assertEq(currentTag, bytes32("PLAGIARISM")); + assertEq(parentDisputeId, 1); + } + function test_DisputeModule_resolveDispute_revert_NotDisputeInitiator() public { vm.expectRevert(Errors.DisputeModule__NotDisputeInitiator.selector); - disputeModule.resolveDispute(1); + disputeModule.resolveDispute(1, ""); } function test_DisputeModule_resolveDispute_revert_NotAbleToResolve() public { @@ -435,7 +549,26 @@ contract DisputeModuleTest is BaseTest { vm.startPrank(ipAccount1); vm.expectRevert(Errors.DisputeModule__NotAbleToResolve.selector); - disputeModule.resolveDispute(1); + disputeModule.resolveDispute(1, ""); + } + + function test_DisputeModule_resolveDispute_revert_ParentDisputeNotResolved() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(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.stopPrank(); + + // tag derivative + disputeModule.tagDerivativeIfParentInfringed(ipAddr, ipAddr2, 1); + + vm.expectRevert(Errors.DisputeModule__ParentDisputeNotResolved.selector); + disputeModule.resolveDispute(2, ""); } function test_DisputeModule_resolveDispute() public { @@ -450,24 +583,24 @@ contract DisputeModuleTest is BaseTest { disputeModule.setDisputeJudgement(1, true, ""); vm.stopPrank(); - (, , , , , bytes32 currentTagBeforeResolve) = disputeModule.disputes(1); + (, , , , , bytes32 currentTagBeforeResolve, ) = disputeModule.disputes(1); // resolve dispute vm.startPrank(ipAccount1); vm.expectEmit(true, true, true, true, address(disputeModule)); emit DisputeResolved(1); - disputeModule.resolveDispute(1); + disputeModule.resolveDispute(1, ""); - (, , , , , bytes32 currentTagAfterResolve) = disputeModule.disputes(1); + (, , , , , bytes32 currentTagAfterResolve, ) = disputeModule.disputes(1); assertEq(currentTagBeforeResolve, bytes32("PLAGIARISM")); assertEq(currentTagAfterResolve, bytes32(0)); assertFalse(disputeModule.isIpTagged(ipAddr)); - // Cant resolve again + // Can't resolve again vm.expectRevert(Errors.DisputeModule__NotAbleToResolve.selector); - disputeModule.resolveDispute(1); + disputeModule.resolveDispute(1, ""); vm.stopPrank(); }