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

Commit

Permalink
Merge branch 'main' into auth-checks
Browse files Browse the repository at this point in the history
  • Loading branch information
jdubpark authored Feb 17, 2024
2 parents 605e4e1 + e3918a8 commit 3bab810
Show file tree
Hide file tree
Showing 20 changed files with 524 additions and 174 deletions.
Binary file modified PIL - Beta - Final_2024-02.pdf
Binary file not shown.
5 changes: 5 additions & 0 deletions contracts/interfaces/modules/dispute/IDisputeModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,9 @@ interface IDisputeModule {
bytes32 targetTag, // The target tag of the dispute
bytes32 currentTag // The current tag of the dispute
);

/// @notice returns true if the ipId is tagged with any tag (meaning at least one dispute went through)
/// @param _ipId The ipId
function isIpTagged(address _ipId) external view returns (bool);

}
5 changes: 5 additions & 0 deletions contracts/interfaces/registries/ILicenseRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,9 @@ interface ILicenseRegistry is IERC1155 {

/// @notice License data (licensor, policy...) for the license id
function license(uint256 licenseId) external view returns (Licensing.License memory);

/// @notice Returns true if the license has been revoked (licensor tagged after a dispute in
/// the dispute module). If the tag is removed, the license is not revoked anymore.
/// @param licenseId The id of the license
function isLicenseRevoked(uint256 licenseId) external view returns (bool);
}
7 changes: 7 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,11 @@ library Errors {
error LicenseRegistry__CallerNotLicensingModule();
error LicenseRegistry__ZeroLicensingModule();
error LicensingModule__CallerNotLicenseRegistry();
error LicenseRegistry__RevokedLicense();
/// @notice emitted when trying to transfer a license that is not transferable (by policy)
error LicenseRegistry__NotTransferable();
/// @notice emitted on constructor if dispute module is not set
error LicenseRegistry__ZeroDisputeModule();

////////////////////////////////////////////////////////////////////////////
// LicensingModule //
Expand Down Expand Up @@ -149,6 +152,10 @@ library Errors {
error LicensingModule__DerivativeRevShareSumExceedsMaxRNFTSupply();
error LicensingModule__MismatchBetweenCommercialRevenueShareAndMinRoyalty();
error LicensingModule__MismatchBetweenRoyaltyPolicy();
/// @notice emitted when trying to interact with an IP that has been disputed in the DisputeModule
error LicensingModule__DisputedIpId();
/// @notice emitted when linking a license from a licensor that has been disputed in the DisputeModule
error LicensingModule__LinkingRevokedLicense();

////////////////////////////////////////////////////////////////////////////
// LicensingModuleAware //
Expand Down
5 changes: 3 additions & 2 deletions contracts/lib/Licensing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ library Licensing {
}

/// @notice Data that define a License Agreement NFT
/// @param policyId Id of the policy this license is based on, which will be set in the derivative
/// IP when the license is burnt
/// @param policyId Id of the policy this license is based on, which will be set in the derivative IP when the
/// license is burnt for linking
/// @param licensorIpId Id of the IP this license is for
/// @param transferable Whether or not the license is transferable
struct License {
uint256 policyId;
address licensorIpId;
Expand Down
18 changes: 17 additions & 1 deletion contracts/modules/dispute-module/DisputeModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.23;

import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";

import { DISPUTE_MODULE_KEY } from "../../lib/modules/Module.sol";
import { BaseModule } from "../../modules/BaseModule.sol";
Expand All @@ -17,6 +18,7 @@ import { ShortStringOps } from "../../utils/ShortStringOps.sol";
/// @notice The Story Protocol dispute module acts as an enforcement layer for
/// that allows to raise disputes and resolve them through arbitration.
contract DisputeModule is IDisputeModule, BaseModule, Governable, ReentrancyGuard, AccessControlled {
using EnumerableSet for EnumerableSet.Bytes32Set;
/// @notice tag to represent the dispute is in dispute state waiting for judgement
bytes32 public constant IN_DISPUTE = bytes32("IN_DISPUTE");

Expand Down Expand Up @@ -54,6 +56,11 @@ contract DisputeModule is IDisputeModule, BaseModule, Governable, ReentrancyGuar
/// @notice Arbitration policy for a given ipId
mapping(address ipId => address arbitrationPolicy) public arbitrationPolicies;

/// @notice counter of successful disputes per ipId
/// @dev BETA feature, for mainnet tag semantics must influence effect in other modules. For now
/// any successful dispute will affect the IP protocol wide
mapping(address ipId => uint256) private successfulDisputesPerIp;

/// @notice Initializes the registration module contract
/// @param _controller The access controller used for IP authorization
/// @param _assetRegistry The address of the IP asset registry
Expand Down Expand Up @@ -189,6 +196,7 @@ contract DisputeModule is IDisputeModule, BaseModule, Governable, ReentrancyGuar

if (_decision) {
disputes[_disputeId].currentTag = dispute.targetTag;
successfulDisputesPerIp[dispute.targetIpId]++;
} else {
disputes[_disputeId].currentTag = bytes32(0);
}
Expand Down Expand Up @@ -219,14 +227,22 @@ contract DisputeModule is IDisputeModule, BaseModule, Governable, ReentrancyGuar
function resolveDispute(uint256 _disputeId) external {
Dispute memory dispute = disputes[_disputeId];

if (dispute.currentTag == IN_DISPUTE) revert Errors.DisputeModule__NotAbleToResolve();
if (msg.sender != dispute.disputeInitiator) revert Errors.DisputeModule__NotDisputeInitiator();
if (dispute.currentTag == IN_DISPUTE || dispute.currentTag == bytes32(0)) revert Errors.DisputeModule__NotAbleToResolve();

successfulDisputesPerIp[dispute.targetIpId]--;
disputes[_disputeId].currentTag = bytes32(0);

emit DisputeResolved(_disputeId);
}


/// @notice returns true if the ipId is tagged with any tag (meaning at least one dispute went through)
/// @param _ipId The ipId
function isIpTagged(address _ipId) external view returns (bool) {
return successfulDisputesPerIp[_ipId] > 0;
}

/// @notice Gets the protocol-wide module identifier for this module
/// @return The dispute module key
function name() public pure override returns (string memory) {
Expand Down
20 changes: 18 additions & 2 deletions contracts/modules/licensing/LicensingModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { IPolicyFrameworkManager } from "../../interfaces/modules/licensing/IPol
import { ILicenseRegistry } from "../../interfaces/registries/ILicenseRegistry.sol";
import { ILicensingModule } from "../../interfaces/modules/licensing/ILicensingModule.sol";
import { IIPAccountRegistry } from "../../interfaces/registries/IIPAccountRegistry.sol";
import { IDisputeModule } from "../../interfaces/modules/dispute/IDisputeModule.sol";
import { Errors } from "../../lib/Errors.sol";
import { DataUniqueness } from "../../lib/DataUniqueness.sol";
import { Licensing } from "../../lib/Licensing.sol";
Expand Down Expand Up @@ -40,6 +41,7 @@ contract LicensingModule is AccessControlled, ILicensingModule, BaseModule, Reen

RoyaltyModule public immutable ROYALTY_MODULE;
ILicenseRegistry public immutable LICENSE_REGISTRY;
IDisputeModule public immutable DISPUTE_MODULE;

string public constant override name = LICENSING_MODULE_KEY;
mapping(address framework => bool registered) private _registeredFrameworkManagers;
Expand All @@ -63,10 +65,12 @@ contract LicensingModule is AccessControlled, ILicensingModule, BaseModule, Reen
address accessController,
address ipAccountRegistry,
address royaltyModule,
address registry
address registry,
address disputeModule
) AccessControlled(accessController, ipAccountRegistry) {
ROYALTY_MODULE = RoyaltyModule(royaltyModule);
LICENSE_REGISTRY = ILicenseRegistry(registry);
DISPUTE_MODULE = IDisputeModule(disputeModule);
}

function licenseRegistry() external view returns (address) {
Expand Down Expand Up @@ -130,6 +134,7 @@ contract LicensingModule is AccessControlled, ILicensingModule, BaseModule, Reen
if (!isPolicyDefined(polId)) {
revert Errors.LicensingModule__PolicyNotFound();
}
_verifyIpNotDisputed(ipId);

indexOnIpId = _addPolicyIdToIp({ ipId: ipId, policyId: polId, isInherited: false, skipIfDuplicate: false });

Expand Down Expand Up @@ -171,10 +176,10 @@ contract LicensingModule is AccessControlled, ILicensingModule, BaseModule, Reen
uint256 amount, // mint amount
address receiver
) external nonReentrant returns (uint256 licenseId) {
// TODO: check if licensor has been tagged by disputer
if (!IP_ACCOUNT_REGISTRY.isIpAccount(licensorIp)) {
revert Errors.LicensingModule__LicensorNotRegistered();
}
_verifyIpNotDisputed(licensorIp);

bool isInherited = _policySetups[licensorIp][policyId].isInherited;
Licensing.Policy memory pol = policy(policyId);
Expand Down Expand Up @@ -262,6 +267,7 @@ contract LicensingModule is AccessControlled, ILicensingModule, BaseModule, Reen
address childIpId,
uint32 minRoyalty
) external nonReentrant verifyPermission(childIpId) {
_verifyIpNotDisputed(childIpId);
address holder = IIPAccount(payable(childIpId)).owner();
address[] memory licensors = new address[](licenseIds.length);
// If royalty policy address is address(0), this means no royalty policy to set.
Expand All @@ -271,6 +277,9 @@ contract LicensingModule is AccessControlled, ILicensingModule, BaseModule, Reen

for (uint256 i = 0; i < licenseIds.length; i++) {
uint256 licenseId = licenseIds[i];
if (LICENSE_REGISTRY.isLicenseRevoked(licenseId)) {
revert Errors.LicensingModule__LinkingRevokedLicense();
}
if (!LICENSE_REGISTRY.isLicensee(licenseId, holder)) {
revert Errors.LicensingModule__NotLicensee();
}
Expand Down Expand Up @@ -556,4 +565,11 @@ contract LicensingModule is AccessControlled, ILicensingModule, BaseModule, Reen
function _policySetPerIpId(bool isInherited, address ipId) private view returns (EnumerableSet.UintSet storage) {
return _policiesPerIpId[keccak256(abi.encode(isInherited, ipId))];
}

function _verifyIpNotDisputed(address ipId) private view {
// TODO: in beta, any tag means revocation, for mainnet we need more context
if (DISPUTE_MODULE.isIpTagged(ipId)) {
revert Errors.LicensingModule__DisputedIpId();
}
}
}
136 changes: 78 additions & 58 deletions contracts/modules/licensing/UMLPolicyFrameworkManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
pragma solidity ^0.8.23;

// external
import { Base64 } from "@openzeppelin/contracts/utils/Base64.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
Expand Down Expand Up @@ -258,86 +257,107 @@ contract UMLPolicyFrameworkManager is
UMLPolicy memory policy = abi.decode(policyData, (UMLPolicy));

/* solhint-disable */
// Follows the OpenSea standard for JSON metadata

// base json
// Follows the OpenSea standard for JSON metadata.
// **Attributions**
string memory json = string(
'{"name": "Story Protocol License NFT", "description": "License agreement stating the terms of a Story Protocol IPAsset", "attributes": ['
);

// Attributions
json = string(
abi.encodePacked(
json,
'{"trait_type": "Attribution", "value": "',
policy.attribution ? "true" : "false",
'"},',
'{"trait_type": "Transferable", "value": "',
policy.transferable ? "true" : "false",
'"},',
'{"trait_type": "Commerical Use", "value": "',
policy.commercialUse ? "true" : "false",
'"},',
'{"trait_type": "commercialAttribution", "value": "',
policy.commercialAttribution ? "true" : "false",
'"},',
'{"trait_type": "commercialRevShare", "value": ',
Strings.toString(policy.commercialRevShare),
"},"
)
);
json = string(
abi.encodePacked(
json,
'{"trait_type": "commercializerCheck", "value": "',
policy.commercializerChecker.toHexString()
)
);
// TODO: add commercializersData?
json = string(
abi.encodePacked(
json,
'"}, {"trait_type": "derivativesAllowed", "value": "',
policy.derivativesAllowed ? "true" : "false",
'"},',
'{"trait_type": "derivativesAttribution", "value": "',
policy.derivativesAttribution ? "true" : "false",
'"},',
'{"trait_type": "derivativesApproval", "value": "',
policy.derivativesApproval ? "true" : "false",
'"},',
'{"trait_type": "derivativesReciprocal", "value": "',
policy.derivativesReciprocal ? "true" : "false",
'"},',
'{"trait_type": "derivativesRevShare", "value": ',
Strings.toString(policy.derivativesRevShare),
"},"
'{"trait_type": "territories", "value": ['
_policyCommercialTraitsToJson(policy),
_policyDerivativeTraitsToJson(policy)
)
);

uint256 territoryCount = policy.territories.length;
for (uint256 i = 0; i < territoryCount; ++i) {
json = string(abi.encodePacked(json, '{"trait_type": "Territories", "value": ['));
uint256 count = policy.territories.length;
for (uint256 i = 0; i < count; ++i) {
json = string(abi.encodePacked(json, '"', policy.territories[i], '"'));
if (i != territoryCount - 1) {
if (i != count - 1) {
// skip comma for last element in the array
json = string(abi.encodePacked(json, ","));
}
}
json = string(abi.encodePacked(json, "]},")); // close the trait_type: "Territories" array

json = string(abi.encodePacked(json, ']}, {"trait_type": "distributionChannels", "value": ['));

uint256 distributionChannelCount = policy.distributionChannels.length;
for (uint256 i = 0; i < distributionChannelCount; ++i) {
json = string(abi.encodePacked(json, '{"trait_type": "Distribution Channels", "value": ['));
count = policy.distributionChannels.length;
for (uint256 i = 0; i < count; ++i) {
json = string(abi.encodePacked(json, '"', policy.distributionChannels[i], '"'));
if (i != distributionChannelCount - 1) {
if (i != count - 1) {
// skip comma for last element in the array
json = string(abi.encodePacked(json, ","));
}
}
json = string(abi.encodePacked(json, "]},")); // close the trait_type: "Distribution Channels" array

// NOTE: (above) last trait added by PFM should have a comma at the end.

/* solhint-enable */

json = string(abi.encodePacked(json, "]}]}"));
return json;
}

/// @notice Encodes the commercial traits of UML policy into a JSON string for OpenSea
function _policyCommercialTraitsToJson(UMLPolicy memory policy) internal pure returns (string memory) {
/* solhint-disable */
// NOTE: TOTAL_RNFT_SUPPLY = 1000 in trait with max_value. For numbers, don't add any display_type, so that
// they will show up in the "Ranking" section of the OpenSea UI.
return
string(
abi.encodePacked(
'{"trait_type": "Attribution", "value": "',
policy.attribution ? "true" : "false",
'"},',
// Skip transferable, it's already added in the common attributes by the LicenseRegistry.
// Should be managed by the LicenseRegistry, not the PFM.
'{"trait_type": "Commerical Use", "value": "',
policy.commercialUse ? "true" : "false",
'"},',
'{"trait_type": "Commercial Attribution", "value": "',
policy.commercialAttribution ? "true" : "false",
'"},',
'{"trait_type": "Commercial Revenue Share", "max_value": 1000, "value": ',
policy.commercialRevShare.toString(),
"},",
'{"trait_type": "Commercializer Check", "value": "',
policy.commercializerChecker.toHexString(),
// Skip on commercializerCheckerData as it's bytes as irrelevant for the user metadata
'"},'
)
);
/* solhint-enable */
}

return string(abi.encodePacked("data:application/json;base64,", Base64.encode(bytes(json))));
/// @notice Encodes the derivative traits of UML policy into a JSON string for OpenSea
function _policyDerivativeTraitsToJson(UMLPolicy memory policy) internal pure returns (string memory) {
/* solhint-disable */
// NOTE: TOTAL_RNFT_SUPPLY = 1000 in trait with max_value. For numbers, don't add any display_type, so that
// they will show up in the "Ranking" section of the OpenSea UI.
return
string(
abi.encodePacked(
'{"trait_type": "Derivatives Allowed", "value": "',
policy.derivativesAllowed ? "true" : "false",
'"},',
'{"trait_type": "Derivatives Attribution", "value": "',
policy.derivativesAttribution ? "true" : "false",
'"},',
'{"trait_type": "Derivatives Approval", "value": "',
policy.derivativesApproval ? "true" : "false",
'"},',
'{"trait_type": "Derivatives Reciprocal", "value": "',
policy.derivativesReciprocal ? "true" : "false",
'"},',
'{"trait_type": "Derivatives Revenue Share", "max_value": 1000, "value": ',
policy.derivativesRevShare.toString(),
"},"
)
);
/* solhint-enable */
}

/// Checks the configuration of commercial use and throws if the policy is not compliant
Expand Down
Loading

0 comments on commit 3bab810

Please sign in to comment.