From 7d4dd28293f24c87695a014c2269722d803e619d Mon Sep 17 00:00:00 2001 From: Raul Date: Wed, 17 Jan 2024 14:04:20 -0800 Subject: [PATCH 01/13] first commit --- contracts/lib/Errors.sol | 7 +++++++ contracts/lib/Licensing.sol | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 contracts/lib/Errors.sol create mode 100644 contracts/lib/Licensing.sol diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol new file mode 100644 index 000000000..57ef82f39 --- /dev/null +++ b/contracts/lib/Errors.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +library Errors { + +} diff --git a/contracts/lib/Licensing.sol b/contracts/lib/Licensing.sol new file mode 100644 index 000000000..bdc6557ba --- /dev/null +++ b/contracts/lib/Licensing.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +library Licensing { + +} From 528e49aa1e00f30d37bdddd9372012fc1eafbcdd Mon Sep 17 00:00:00 2001 From: Raul Date: Thu, 18 Jan 2024 12:11:53 -0800 Subject: [PATCH 02/13] remove Counter --- contracts/Counter.sol | 14 -------------- test/foundry/Counter.t.sol | 24 ------------------------ 2 files changed, 38 deletions(-) delete mode 100644 contracts/Counter.sol delete mode 100644 test/foundry/Counter.t.sol diff --git a/contracts/Counter.sol b/contracts/Counter.sol deleted file mode 100644 index aded7997b..000000000 --- a/contracts/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/test/foundry/Counter.t.sol b/test/foundry/Counter.t.sol deleted file mode 100644 index 91a51256a..000000000 --- a/test/foundry/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import { Test } from "forge-std/Test.sol"; -import { Counter } from "../../contracts/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function testIncrement() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testSetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} From 1e1b34de84220fe3b7c004eba50ccc970dfa7d49 Mon Sep 17 00:00:00 2001 From: Raul Date: Thu, 18 Jan 2024 16:03:58 -0800 Subject: [PATCH 03/13] add framework --- .../interfaces/licensing/IParamVerifier.sol | 9 + contracts/lib/Errors.sol | 12 ++ contracts/lib/Licensing.sol | 49 +++++ contracts/registries/LicenseRegistry.sol | 170 ++++++++++++++++++ test/foundry/LicenseRegistry.t.sol | 59 ++++++ .../mocks/licensing/MockParamVerifier.sol | 16 ++ 6 files changed, 315 insertions(+) create mode 100644 contracts/interfaces/licensing/IParamVerifier.sol create mode 100644 contracts/registries/LicenseRegistry.sol create mode 100644 test/foundry/LicenseRegistry.t.sol create mode 100644 test/foundry/mocks/licensing/MockParamVerifier.sol diff --git a/contracts/interfaces/licensing/IParamVerifier.sol b/contracts/interfaces/licensing/IParamVerifier.sol new file mode 100644 index 000000000..1cd667090 --- /dev/null +++ b/contracts/interfaces/licensing/IParamVerifier.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +interface IParamVerifier { + + function verifyParam(address caller, bytes memory value) external view returns (bool); + function json() external view returns (string memory); +} diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 57ef82f39..227119f38 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -2,6 +2,18 @@ pragma solidity ^0.8.20; +/// @title Errors Library +/// @notice Library for all Story Protocol contract errors. library Errors { + //////////////////////////////////////////////////////////////////////////// + // LicenseRegistry // + //////////////////////////////////////////////////////////////////////////// + + /// @notice Error thrown when a policy is already set for an IP ID. + error LicenseRegistry__PolicyAlreadySetForIpId(); + error LicenseRegistry__EmptyLicenseUrl(); + error LicenseRegistry__ParamVerifierLengthMismatch(); + error LicenseRegistry__InvalidParamVerifierType(); + } diff --git a/contracts/lib/Licensing.sol b/contracts/lib/Licensing.sol index bdc6557ba..164860157 100644 --- a/contracts/lib/Licensing.sol +++ b/contracts/lib/Licensing.sol @@ -1,7 +1,56 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; +import { IParamVerifier } from "../interfaces/licensing/IParamVerifier.sol"; library Licensing { + + struct Parameter { + IParamVerifier verifier; + bytes defaultValue; + } + + enum ParamVerifierType { + Minting, + Activate, + LinkParent + } + + struct Framework { + // These parameters need to be verified when minting a license + Parameter[] mintingParams; + // License may need to be activated before linking, these parameters must be verified to activate. + Parameter[] activationParams; + // If the framework defaults to not needing activation, this can be set to true to skip activateParams check.abi + bool defaultNeedsActivation; + // These parameters need to be verified so the owner of a license can link to a parent ipId/policy + Parameter[] linkParentParams; + string licenseUrl; + } + + // Needed because Solidity doesn't support passing nested struct arrays to storage + struct FrameworkCreationParams { + IParamVerifier[] mintingParamVerifiers; + bytes[] mintingParamDefaultValues; + IParamVerifier[] activationParamVerifiers; + bytes[] activationParamDefaultValues; + bool defaultNeedsActivation; + IParamVerifier[] linkParentParamVerifiers; + bytes[] linkParentParamDefaultValues; + string licenseUrl; + } + struct Policy { + uint256 frameworkId; + bytes[] mintingParamValues; + bytes[] activationParamValues; + // must be set to true if policy will mint licenses without the need for activation + bool needsActivation; + bytes[] linkParentParamValues; + } + + struct License { + uint256 policyId; + address[] licensorIpIds; + } } diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol new file mode 100644 index 000000000..fbdd33605 --- /dev/null +++ b/contracts/registries/LicenseRegistry.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { Licensing } from "../lib/Licensing.sol"; +import { IParamVerifier } from "../interfaces/licensing/IParamVerifier.sol"; +import { Errors } from "../lib/Errors.sol"; +import { ERC1155 } from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; +import { ERC1155Burnable } from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol"; +import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; + +contract LicenseRegistry is ERC1155, ERC1155Burnable { + + using EnumerableSet for EnumerableSet.UintSet; + using Strings for *; + + mapping(uint256 => Licensing.Framework) private _frameworks; + uint256 private _totalFrameworks; + + mapping(bytes32 => uint256) private _hashedPolicies; + mapping(uint256 => Licensing.Policy) private _policies; + uint256 private _totalPolicies; + + mapping(address => EnumerableSet.UintSet) private _policiesPerIpId; + mapping(address => address[]) private _ipParents; + + mapping(uint256 => Licensing.License) private _licenses; + uint256 private _totalLicenses; + + constructor(string memory uri) ERC1155(uri) {} + + // Protocol available terms + function addLicenseFramework(Licensing.FrameworkCreationParams calldata fwCreation) external returns(uint256 frameworkId) { + if (bytes(fwCreation.licenseUrl).length == 0 || fwCreation.licenseUrl.equal("")) { + revert Errors.LicenseRegistry__EmptyLicenseUrl(); + } + // check protocol auth + _totalFrameworks++; + _frameworks[_totalFrameworks].licenseUrl = fwCreation.licenseUrl; + _frameworks[_totalFrameworks].defaultNeedsActivation = fwCreation.defaultNeedsActivation; + _setParamArray(_frameworks[_totalFrameworks], Licensing.ParamVerifierType.Minting, fwCreation.mintingParamVerifiers, fwCreation.mintingParamDefaultValues); + _setParamArray(_frameworks[_totalFrameworks], Licensing.ParamVerifierType.Activate, fwCreation.activationParamVerifiers, fwCreation.activationParamDefaultValues); + _setParamArray(_frameworks[_totalFrameworks], Licensing.ParamVerifierType.LinkParent, fwCreation.linkParentParamVerifiers, fwCreation.linkParentParamDefaultValues); + // Should we add a label? + // TODO: emit + return _totalFrameworks; + } + + function _setParamArray( + Licensing.Framework storage fw, + Licensing.ParamVerifierType pvType, + IParamVerifier[] calldata paramVerifiers, + bytes[] calldata paramDefaultValues + ) private { + if (paramVerifiers.length != paramDefaultValues.length) { + revert Errors.LicenseRegistry__ParamVerifierLengthMismatch(); + } + Licensing.Parameter[] storage params; + if (pvType == Licensing.ParamVerifierType.Minting) { + params = fw.mintingParams; + } else if (pvType == Licensing.ParamVerifierType.Activate) { + params = fw.activationParams; + } else if (pvType == Licensing.ParamVerifierType.LinkParent) { + params = fw.linkParentParams; + } else { + revert Errors.LicenseRegistry__InvalidParamVerifierType(); + } + for (uint256 i = 0; i < paramVerifiers.length; i++) { + params.push(Licensing.Parameter({ + verifier: paramVerifiers[i], + defaultValue: paramDefaultValues[i] + })); + } + } + + function totalFrameworks() external view returns(uint256) { + return _totalFrameworks; + } + + function framework(uint256 frameworkId) external view returns(Licensing.Framework memory) { + return _frameworks[frameworkId]; + } + + function _addPolicyIdOrGetExisting(Licensing.Policy calldata pol) internal returns(uint256) { + // We could just use the hash of the policy as id to save some gas, but the UX/DX of having huge random + // numbers for ID is bad enough to justify the cost, plus we have accountability on current number of + // policies. + bytes32 policyHash = keccak256(abi.encode(pol)); + uint256 id = _hashedPolicies[policyHash]; + if (id != 0) { + return id; + } + id = ++_totalPolicies; + _hashedPolicies[policyHash] = id; + _policies[id] = pol; + // TODO: emit + return id; + } + + + // Per IP ID + // Sets available policies per IPID, returns policy Id. This is not a license. + function addPolicy(address ipId, Licensing.Policy calldata pol) external returns(uint256 policyId, uint256 indexOnIpId) { + // check protocol auth + Licensing.Framework memory fw = _frameworks[pol.frameworkId]; + + policyId = _addPolicyIdOrGetExisting(pol); + return (policyId, _addPolictyId(ipId, policyId)); + } + + function _addPolictyId(address ipId, uint256 policyId) internal returns(uint256 index) { + EnumerableSet.UintSet storage policySet = _policiesPerIpId[ipId]; + if (!policySet.add(policyId)) { + revert Errors.LicenseRegistry__PolicyAlreadySetForIpId(); + } + // TODO: emit + return policySet.length(); + } + + // function _addPolictyId(address ipId, uint256 policyId) internal returns(uint256 index) { + // // TODO check for existance of ID before calling internal method + // } + + function totalPolicies() external view returns(uint256) { + return _totalPolicies; + } + + function policy(uint256 policyId) external view returns(Licensing.Policy memory) { + return _policies[policyId]; + } + + /// @dev potentially expensive operation, use with care + function policyIdsForIp(address ipId) external view returns(uint256[] memory policyIds) { + return _policiesPerIpId[ipId].values(); + } + + function totalPoliciesForIp(address ipId) external view returns(uint256) { + return _policiesPerIpId[ipId].length(); + } + + function isPolicyIdSetForIp(address ipId, uint256 policyId) external view returns(bool) { + return _policiesPerIpId[ipId].contains(policyId); + } + + function policyIdForIpAtIndex(address ipId, uint256 index) external view returns(uint256 policyId) { + return _policiesPerIpId[ipId].at(index); + } + + function policyForIpAtIndex(address ipId, uint256 index) external view returns(Licensing.Policy memory) { + return _policies[_policiesPerIpId[ipId].at(index)]; + } + + // function linkIpaToParent(address ipId, address parentIpId, uint256 licenseId) external { + // // Check if parent is tagged + // if (_ownerOf(licenseId) != msg.sender) { + // revert "Not owner of license"; + // } + + // // if msg.sender == parent ipa or correct licensor + // for (uint i = 0; i < conditions.length; i++) { + // if (!conditions[i].isFulfilled(msg.sender, ) { + // revert PreConditionNotFulfilled(); + // } + // } + // child.addParent(parent); + // _burn + // _mint + // } +} \ No newline at end of file diff --git a/test/foundry/LicenseRegistry.t.sol b/test/foundry/LicenseRegistry.t.sol new file mode 100644 index 000000000..d30c9d626 --- /dev/null +++ b/test/foundry/LicenseRegistry.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import { Test } from "forge-std/Test.sol"; +import { LicenseRegistry } from "contracts/registries/LicenseRegistry.sol"; +import { Licensing } from "contracts/lib/Licensing.sol"; +import { MockIParamVerifier } from "test/foundry/mocks/licensing/MockParamVerifier.sol"; +import { IParamVerifier } from "contracts/interfaces/licensing/IParamVerifier.sol"; + +contract LicenseRegistryTest is Test { + LicenseRegistry public registry; + Licensing.Framework public framework; + enum VerifierType { + Minting, + Activate, + LinkParent + } + MockIParamVerifier public verifier; + string public licenseUrl = "https://example.com/license"; + Licensing.FrameworkCreationParams fwParams; + + modifier withFrameworkParams() { + IParamVerifier[] memory mintingParamVerifiers = new IParamVerifier[](1); + mintingParamVerifiers[0] = verifier; + bytes[] memory mintingParamDefaultValues = new bytes[](1); + mintingParamDefaultValues[0] = abi.encode(true); + IParamVerifier[] memory activationParamVerifiers = new IParamVerifier[](1); + activationParamVerifiers[0] = verifier; + bytes[] memory activationParamDefaultValues = new bytes[](1); + activationParamDefaultValues[0] = abi.encode(true); + IParamVerifier[] memory linkParentParamVerifiers = new IParamVerifier[](1); + linkParentParamVerifiers[0] = verifier; + bytes[] memory linkParentParamDefaultValues = new bytes[](1); + linkParentParamDefaultValues[0] = abi.encode(true); + + fwParams = Licensing.FrameworkCreationParams({ + mintingParamVerifiers: mintingParamVerifiers, + mintingParamDefaultValues: mintingParamDefaultValues, + activationParamVerifiers: activationParamVerifiers, + activationParamDefaultValues: activationParamDefaultValues, + defaultNeedsActivation: true, + linkParentParamVerifiers: linkParentParamVerifiers, + linkParentParamDefaultValues: linkParentParamDefaultValues, + licenseUrl: licenseUrl + }); + _; + } + + function setUp() public { + verifier = new MockIParamVerifier(); + registry = new LicenseRegistry("https://example.com/{id}.json"); + } + + function test_LicenseRegistry_addLicenseFramework() public withFrameworkParams { + registry.addLicenseFramework(fwParams); + assertEq(keccak256(abi.encode(registry.framework(0))), keccak256(abi.encode(framework)), "framework not added"); + assertEq(registry.totalFrameworks(), 1, "total frameworks not updated"); + } +} diff --git a/test/foundry/mocks/licensing/MockParamVerifier.sol b/test/foundry/mocks/licensing/MockParamVerifier.sol new file mode 100644 index 000000000..b1a2df3a5 --- /dev/null +++ b/test/foundry/mocks/licensing/MockParamVerifier.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { IParamVerifier } from "contracts/interfaces/licensing/IParamVerifier.sol"; + +contract MockIParamVerifier is IParamVerifier { + + function verifyParam(address, bytes memory value) external pure returns (bool) { + return abi.decode(value, (bool)); + } + + function json() external pure returns (string memory) { + return ""; + } +} From 7eebe5883c3b439f2ad3a0d041e7b02fb82cd57b Mon Sep 17 00:00:00 2001 From: Raul Date: Thu, 18 Jan 2024 16:06:04 -0800 Subject: [PATCH 04/13] refactor test modifier --- test/foundry/LicenseRegistry.t.sol | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/foundry/LicenseRegistry.t.sol b/test/foundry/LicenseRegistry.t.sol index d30c9d626..d468b8f86 100644 --- a/test/foundry/LicenseRegistry.t.sol +++ b/test/foundry/LicenseRegistry.t.sol @@ -20,6 +20,12 @@ contract LicenseRegistryTest is Test { Licensing.FrameworkCreationParams fwParams; modifier withFrameworkParams() { + _initFwParams(); + registry.addLicenseFramework(fwParams); + _; + } + + function _initFwParams() private { IParamVerifier[] memory mintingParamVerifiers = new IParamVerifier[](1); mintingParamVerifiers[0] = verifier; bytes[] memory mintingParamDefaultValues = new bytes[](1); @@ -43,7 +49,6 @@ contract LicenseRegistryTest is Test { linkParentParamDefaultValues: linkParentParamDefaultValues, licenseUrl: licenseUrl }); - _; } function setUp() public { @@ -51,7 +56,8 @@ contract LicenseRegistryTest is Test { registry = new LicenseRegistry("https://example.com/{id}.json"); } - function test_LicenseRegistry_addLicenseFramework() public withFrameworkParams { + function test_LicenseRegistry_addLicenseFramework() public { + _initFwParams(); registry.addLicenseFramework(fwParams); assertEq(keccak256(abi.encode(registry.framework(0))), keccak256(abi.encode(framework)), "framework not added"); assertEq(registry.totalFrameworks(), 1, "total frameworks not updated"); From 7357ada8d9ea5f2d925fca0212b1e52981af2d76 Mon Sep 17 00:00:00 2001 From: Raul Date: Thu, 18 Jan 2024 16:58:21 -0800 Subject: [PATCH 05/13] test add policies, fix id setting --- contracts/registries/LicenseRegistry.sol | 4 +- test/foundry/LicenseRegistry.t.sol | 73 ++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index fbdd33605..2318d9e69 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -36,7 +36,7 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { revert Errors.LicenseRegistry__EmptyLicenseUrl(); } // check protocol auth - _totalFrameworks++; + ++_totalFrameworks; _frameworks[_totalFrameworks].licenseUrl = fwCreation.licenseUrl; _frameworks[_totalFrameworks].defaultNeedsActivation = fwCreation.defaultNeedsActivation; _setParamArray(_frameworks[_totalFrameworks], Licensing.ParamVerifierType.Minting, fwCreation.mintingParamVerifiers, fwCreation.mintingParamDefaultValues); @@ -115,7 +115,7 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { revert Errors.LicenseRegistry__PolicyAlreadySetForIpId(); } // TODO: emit - return policySet.length(); + return policySet.length() - 1; } // function _addPolictyId(address ipId, uint256 policyId) internal returns(uint256 index) { diff --git a/test/foundry/LicenseRegistry.t.sol b/test/foundry/LicenseRegistry.t.sol index d468b8f86..bd35f7167 100644 --- a/test/foundry/LicenseRegistry.t.sol +++ b/test/foundry/LicenseRegistry.t.sol @@ -18,6 +18,9 @@ contract LicenseRegistryTest is Test { MockIParamVerifier public verifier; string public licenseUrl = "https://example.com/license"; Licensing.FrameworkCreationParams fwParams; + address public ipId1 = address(0x111); + address public ipId2 = address(0x222); + modifier withFrameworkParams() { _initFwParams(); @@ -62,4 +65,74 @@ contract LicenseRegistryTest is Test { assertEq(keccak256(abi.encode(registry.framework(0))), keccak256(abi.encode(framework)), "framework not added"); assertEq(registry.totalFrameworks(), 1, "total frameworks not updated"); } + + function test_LicenseRegistry_addPolicyId() public withFrameworkParams { + Licensing.Policy memory policy = Licensing.Policy({ + frameworkId: 0, + mintingParamValues: new bytes[](1), + activationParamValues: new bytes[](1), + needsActivation: false, + linkParentParamValues: new bytes[](1) + }); + policy.mintingParamValues[0] = abi.encode("test"); + policy.activationParamValues[0] = abi.encode("test"); + policy.linkParentParamValues[0] = abi.encode("test"); + (uint256 policyId, uint256 indexOnIpId) = registry.addPolicy(ipId1, policy); + assertEq(policyId, 1, "policyId not 1"); + assertEq(indexOnIpId, 0, "indexOnIpId not 0"); + Licensing.Policy memory storedPolicy = registry.policy(policyId); + assertEq(keccak256(abi.encode(storedPolicy)), keccak256(abi.encode(policy)), "policy not stored properly"); + + } + + function test_LicenseRegistry_addSamePolicyReusesPolicyId() public withFrameworkParams { + Licensing.Policy memory policy = Licensing.Policy({ + frameworkId: 0, + mintingParamValues: new bytes[](1), + activationParamValues: new bytes[](1), + needsActivation: false, + linkParentParamValues: new bytes[](1) + }); + policy.mintingParamValues[0] = abi.encode("test"); + policy.activationParamValues[0] = abi.encode("test"); + policy.linkParentParamValues[0] = abi.encode("test"); + (uint256 policyId, uint256 indexOnIpId) = registry.addPolicy(ipId1, policy); + (uint256 policyId2, uint256 indexOnIpId2) = registry.addPolicy(ipId2, policy); + assertEq(policyId, policyId2, "policyId not reused"); + } + + //function test_LicenseRegistry_revert_policyAlreadyAddedToIpId() + + function test_LicenseRegistry_add2PoliciesToIpId() public withFrameworkParams { + assertEq(registry.totalPolicies(), 0); + assertEq(registry.totalPoliciesForIp(ipId1), 0); + Licensing.Policy memory policy = Licensing.Policy({ + frameworkId: 0, + mintingParamValues: new bytes[](1), + activationParamValues: new bytes[](1), + needsActivation: false, + linkParentParamValues: new bytes[](1) + }); + policy.mintingParamValues[0] = abi.encode("test"); + policy.activationParamValues[0] = abi.encode("test"); + policy.linkParentParamValues[0] = abi.encode("test"); + + // First time adding a policy + (uint256 policyId, uint256 indexOnIpId) = registry.addPolicy(ipId1, policy); + assertEq(policyId, 1, "policyId not 1"); + assertEq(indexOnIpId, 0, "indexOnIpId not 0"); + assertEq(registry.totalPolicies(), 1, "totalPolicies not incremented"); + assertEq(registry.totalPoliciesForIp(ipId1), 1, "totalPoliciesForIp not incremented"); + assertEq(registry.policyIdForIpAtIndex(ipId1, 0), 1, "policyIdForIpAtIndex not 1"); + + // Adding different policy to same ipId + policy.mintingParamValues[0] = abi.encode("test2"); + (uint256 policyId2, uint256 indexOnIpId2) = registry.addPolicy(ipId1, policy); + assertEq(policyId2, 2, "policyId not 2"); + assertEq(indexOnIpId2, 1, "indexOnIpId not 1"); + assertEq(registry.totalPolicies(), 2, "totalPolicies not incremented"); + assertEq(registry.totalPoliciesForIp(ipId1), 2, "totalPoliciesForIp not incremented"); + assertEq(registry.policyIdForIpAtIndex(ipId1, 1), 2, "policyIdForIpAtIndex not 2"); + } + } From 17259e6d7f0815334c41998ce970f18ea88e576c Mon Sep 17 00:00:00 2001 From: Raul Date: Thu, 18 Jan 2024 18:45:46 -0800 Subject: [PATCH 06/13] fix test and add minting method --- contracts/lib/Errors.sol | 3 +- contracts/registries/LicenseRegistry.sol | 101 ++++++++++++++++------- 2 files changed, 71 insertions(+), 33 deletions(-) diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 227119f38..fe26da64a 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -15,5 +15,6 @@ library Errors { error LicenseRegistry__EmptyLicenseUrl(); error LicenseRegistry__ParamVerifierLengthMismatch(); error LicenseRegistry__InvalidParamVerifierType(); - + error LicenseRegistry__PolicyNotFound(); + error LicenseRegistry__NotLicensee(); } diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index 2318d9e69..491a0c858 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -10,6 +10,9 @@ import { ERC1155Burnable } from "@openzeppelin/contracts/token/ERC1155/extension import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import "forge-std/console2.sol"; + +// TODO: consider disabling operators/approvals on creation contract LicenseRegistry is ERC1155, ERC1155Burnable { using EnumerableSet for EnumerableSet.UintSet; @@ -21,21 +24,34 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { mapping(bytes32 => uint256) private _hashedPolicies; mapping(uint256 => Licensing.Policy) private _policies; uint256 private _totalPolicies; - + // DO NOT remove policies, that rugs derivatives and breaks ordering assumptions in set mapping(address => EnumerableSet.UintSet) private _policiesPerIpId; mapping(address => address[]) private _ipParents; + mapping(bytes32 => uint256) private _hashedLicenses; mapping(uint256 => Licensing.License) private _licenses; + + /// This tracks the number of licenses registered in the protocol, it will not decrease when a license is burnt. uint256 private _totalLicenses; + modifier onlyLicensee(uint256 licenseId, address holder) { + // Should ERC1155 operator count? IMO is a security risk. Better use ACL + if (balanceOf(holder, licenseId) == 0) { + revert Errors.LicenseRegistry__NotLicensee(); + } + _; + } + constructor(string memory uri) ERC1155(uri) {} // Protocol available terms function addLicenseFramework(Licensing.FrameworkCreationParams calldata fwCreation) external returns(uint256 frameworkId) { + // check protocol auth if (bytes(fwCreation.licenseUrl).length == 0 || fwCreation.licenseUrl.equal("")) { revert Errors.LicenseRegistry__EmptyLicenseUrl(); } - // check protocol auth + // Todo: check duplications + ++_totalFrameworks; _frameworks[_totalFrameworks].licenseUrl = fwCreation.licenseUrl; _frameworks[_totalFrameworks].defaultNeedsActivation = fwCreation.defaultNeedsActivation; @@ -82,22 +98,22 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { return _frameworks[frameworkId]; } - function _addPolicyIdOrGetExisting(Licensing.Policy calldata pol) internal returns(uint256) { + function _addIdOrGetExisting(bytes memory data, mapping(bytes32 => uint256) storage _hashToIds, uint256 existingIds) private returns(uint256 id, bool isNew) { // We could just use the hash of the policy as id to save some gas, but the UX/DX of having huge random // numbers for ID is bad enough to justify the cost, plus we have accountability on current number of // policies. - bytes32 policyHash = keccak256(abi.encode(pol)); - uint256 id = _hashedPolicies[policyHash]; + console2.log("existingIds", existingIds); + + bytes32 hash = keccak256(data); + uint256 id = _hashToIds[hash]; if (id != 0) { - return id; + return (id, false); } - id = ++_totalPolicies; - _hashedPolicies[policyHash] = id; - _policies[id] = pol; - // TODO: emit - return id; + id = existingIds + 1; + console2.log("returningId", id); + _hashToIds[hash] = id; + return (id, true); } - // Per IP ID // Sets available policies per IPID, returns policy Id. This is not a license. @@ -105,12 +121,21 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { // check protocol auth Licensing.Framework memory fw = _frameworks[pol.frameworkId]; - policyId = _addPolicyIdOrGetExisting(pol); + (uint256 polId, bool isNew) = _addIdOrGetExisting(abi.encode(pol), _hashedPolicies, _totalPolicies); + policyId = polId; + if (isNew) { + _totalPolicies = polId; + console2.log("_totalPolicies", _totalPolicies); + console2.log("polId", polId); + _policies[polId] = pol; + // TODO: emit + } return (policyId, _addPolictyId(ipId, policyId)); } function _addPolictyId(address ipId, uint256 policyId) internal returns(uint256 index) { EnumerableSet.UintSet storage policySet = _policiesPerIpId[ipId]; + // TODO: check if policy is compatible with the others if (!policySet.add(policyId)) { revert Errors.LicenseRegistry__PolicyAlreadySetForIpId(); } @@ -118,9 +143,6 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { return policySet.length() - 1; } - // function _addPolictyId(address ipId, uint256 policyId) internal returns(uint256 index) { - // // TODO check for existance of ID before calling internal method - // } function totalPolicies() external view returns(uint256) { return _totalPolicies; @@ -151,20 +173,35 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { return _policies[_policiesPerIpId[ipId].at(index)]; } - // function linkIpaToParent(address ipId, address parentIpId, uint256 licenseId) external { - // // Check if parent is tagged - // if (_ownerOf(licenseId) != msg.sender) { - // revert "Not owner of license"; - // } - - // // if msg.sender == parent ipa or correct licensor - // for (uint i = 0; i < conditions.length; i++) { - // if (!conditions[i].isFulfilled(msg.sender, ) { - // revert PreConditionNotFulfilled(); - // } - // } - // child.addParent(parent); - // _burn - // _mint - // } + function mintLicense(Licensing.License calldata licenseData, uint256 amount, address receiver) external returns(uint256 licenseId) { + for(uint256 i = 0; i < licenseData.licensorIpIds.length; i++) { + // TODO: check if licensors are valid IP Ids and if they have been tagged bad + // TODO: check if licensor allowed sender to mint in their behalf + } + + uint256 polId = licenseData.policyId; + Licensing.Policy memory pol = _policies[polId]; + if (pol.frameworkId == 0 || pol.frameworkId > _totalFrameworks) { + revert Errors.LicenseRegistry__PolicyNotFound(); + } + + // TODO: execute minting params to check if they are valid + + (uint256 lId, bool isNew) = _addIdOrGetExisting(abi.encode(pol), _hashedLicenses, _totalLicenses); + licenseId = lId; + if (isNew) { + _totalLicenses = licenseId; + _licenses[licenseId] = licenseData; + // TODO: emit + } + _mint(receiver, licenseId, amount, ""); + return licenseId; + } + + function isLicensee(uint256 licenseId, address holder) external view returns(bool) { + return balanceOf(holder, licenseId) > 0; + } + + // TODO: activation method + } \ No newline at end of file From b94c71142c4663a5f5f364bb975ead680124b3ca Mon Sep 17 00:00:00 2001 From: Raul Date: Thu, 18 Jan 2024 21:40:46 -0800 Subject: [PATCH 07/13] license minting --- contracts/registries/LicenseRegistry.sol | 8 ++--- test/foundry/LicenseRegistry.t.sol | 42 ++++++++++++++++++++---- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index 491a0c858..af9d5b26a 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -53,6 +53,7 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { // Todo: check duplications ++_totalFrameworks; + console2.log("_totalFrameworks", _totalFrameworks); _frameworks[_totalFrameworks].licenseUrl = fwCreation.licenseUrl; _frameworks[_totalFrameworks].defaultNeedsActivation = fwCreation.defaultNeedsActivation; _setParamArray(_frameworks[_totalFrameworks], Licensing.ParamVerifierType.Minting, fwCreation.mintingParamVerifiers, fwCreation.mintingParamDefaultValues); @@ -94,7 +95,7 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { return _totalFrameworks; } - function framework(uint256 frameworkId) external view returns(Licensing.Framework memory) { + function framework(uint256 frameworkId) external view returns(Licensing.Framework memory framework) { return _frameworks[frameworkId]; } @@ -102,15 +103,12 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { // We could just use the hash of the policy as id to save some gas, but the UX/DX of having huge random // numbers for ID is bad enough to justify the cost, plus we have accountability on current number of // policies. - console2.log("existingIds", existingIds); - bytes32 hash = keccak256(data); uint256 id = _hashToIds[hash]; if (id != 0) { return (id, false); } id = existingIds + 1; - console2.log("returningId", id); _hashToIds[hash] = id; return (id, true); } @@ -125,8 +123,6 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { policyId = polId; if (isNew) { _totalPolicies = polId; - console2.log("_totalPolicies", _totalPolicies); - console2.log("polId", polId); _policies[polId] = pol; // TODO: emit } diff --git a/test/foundry/LicenseRegistry.t.sol b/test/foundry/LicenseRegistry.t.sol index bd35f7167..ad8a9d7a5 100644 --- a/test/foundry/LicenseRegistry.t.sol +++ b/test/foundry/LicenseRegistry.t.sol @@ -20,8 +20,9 @@ contract LicenseRegistryTest is Test { Licensing.FrameworkCreationParams fwParams; address public ipId1 = address(0x111); address public ipId2 = address(0x222); + address public licensee = address(0x101); - + // TODO: add parameter config for initial framework for 100% test modifier withFrameworkParams() { _initFwParams(); registry.addLicenseFramework(fwParams); @@ -61,14 +62,18 @@ contract LicenseRegistryTest is Test { function test_LicenseRegistry_addLicenseFramework() public { _initFwParams(); - registry.addLicenseFramework(fwParams); - assertEq(keccak256(abi.encode(registry.framework(0))), keccak256(abi.encode(framework)), "framework not added"); + uint256 fwId = registry.addLicenseFramework(fwParams); + Licensing.Framework memory fw = registry.framework(fwId); + assertEq(fwId, 1, "not incrementing fw id"); + assertEq(keccak256(abi.encode(fw.licenseUrl)), keccak256(abi.encode(fwParams.licenseUrl))); + assertEq(fw.defaultNeedsActivation, fwParams.defaultNeedsActivation); + // TODO: test Parameter[] vs IParamVerifier[] && bytes[] assertEq(registry.totalFrameworks(), 1, "total frameworks not updated"); } function test_LicenseRegistry_addPolicyId() public withFrameworkParams { Licensing.Policy memory policy = Licensing.Policy({ - frameworkId: 0, + frameworkId: 1, mintingParamValues: new bytes[](1), activationParamValues: new bytes[](1), needsActivation: false, @@ -87,7 +92,7 @@ contract LicenseRegistryTest is Test { function test_LicenseRegistry_addSamePolicyReusesPolicyId() public withFrameworkParams { Licensing.Policy memory policy = Licensing.Policy({ - frameworkId: 0, + frameworkId: 1, mintingParamValues: new bytes[](1), activationParamValues: new bytes[](1), needsActivation: false, @@ -107,7 +112,7 @@ contract LicenseRegistryTest is Test { assertEq(registry.totalPolicies(), 0); assertEq(registry.totalPoliciesForIp(ipId1), 0); Licensing.Policy memory policy = Licensing.Policy({ - frameworkId: 0, + frameworkId: 1, mintingParamValues: new bytes[](1), activationParamValues: new bytes[](1), needsActivation: false, @@ -135,4 +140,29 @@ contract LicenseRegistryTest is Test { assertEq(registry.policyIdForIpAtIndex(ipId1, 1), 2, "policyIdForIpAtIndex not 2"); } + function test_LicenseRegistry_mintLicense() public withFrameworkParams { + Licensing.Policy memory policy = Licensing.Policy({ + frameworkId: 1, + mintingParamValues: new bytes[](1), + activationParamValues: new bytes[](1), + needsActivation: false, + linkParentParamValues: new bytes[](1) + }); + policy.mintingParamValues[0] = abi.encode("test"); + policy.activationParamValues[0] = abi.encode("test"); + policy.linkParentParamValues[0] = abi.encode("test"); + (uint256 policyId, uint256 indexOnIpId) = registry.addPolicy(ipId1, policy); + + Licensing.License memory licenseData = Licensing.License({ + policyId: policyId, + licensorIpIds: new address[](1) + }); + licenseData.licensorIpIds[0] = ipId2; + + uint256 licenseId = registry.mintLicense(licenseData, 2, licensee); + assertEq(licenseId, 1); + assertEq(registry.balanceOf(licensee, licenseId), 2); + } + + } From 18e65d4d96bbf3a377630c91d90e35162f704db2 Mon Sep 17 00:00:00 2001 From: Raul Date: Fri, 19 Jan 2024 01:39:55 -0800 Subject: [PATCH 08/13] natspec --- contracts/lib/Errors.sol | 3 + contracts/lib/Licensing.sol | 33 ++++- contracts/registries/LicenseRegistry.sol | 150 ++++++++++++++++++++--- test/foundry/LicenseRegistry.t.sol | 27 +++- 4 files changed, 182 insertions(+), 31 deletions(-) diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index fe26da64a..86441b782 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -12,9 +12,12 @@ library Errors { /// @notice Error thrown when a policy is already set for an IP ID. error LicenseRegistry__PolicyAlreadySetForIpId(); + error LicenseRegistry__FrameworkNotFound(); error LicenseRegistry__EmptyLicenseUrl(); error LicenseRegistry__ParamVerifierLengthMismatch(); error LicenseRegistry__InvalidParamVerifierType(); error LicenseRegistry__PolicyNotFound(); error LicenseRegistry__NotLicensee(); + error LicenseRegistry__ParentIdEqualThanChild(); + error LicenseRegistry__LicensorDoesntHaveThisPolicy(); } diff --git a/contracts/lib/Licensing.sol b/contracts/lib/Licensing.sol index 164860157..63cede566 100644 --- a/contracts/lib/Licensing.sol +++ b/contracts/lib/Licensing.sol @@ -5,26 +5,36 @@ import { IParamVerifier } from "../interfaces/licensing/IParamVerifier.sol"; library Licensing { + /// Identifies a license parameter (term) from a license framework struct Parameter { + /// Contract that must check if the condition of the paremeter is set IParamVerifier verifier; + /// Default value for the parameter, as defined in the license framework text bytes defaultValue; } + /// Moment of the license lifetime where a Parameter will be verified enum ParamVerifierType { Minting, Activate, LinkParent } + /// Describes a licensing framework, which is a set of licensing terms (parameters) + /// that come into effect in different moments of the licensing life cycle. + /// Must correspond to human (or at least lawyer) readable text describing them in licenseUrl. + /// To be valid in Story Protocol, the parameters described in the text must express default values + /// corresponding to those of each Parameter struct struct Framework { - // These parameters need to be verified when minting a license + /// These parameters need to be verified when minting a license Parameter[] mintingParams; - // License may need to be activated before linking, these parameters must be verified to activate. + /// License may need to be activated before linking, these parameters must be verified to activate. Parameter[] activationParams; - // If the framework defaults to not needing activation, this can be set to true to skip activateParams check.abi + /// If the framework defaults to not needing activation, this can be set to true to skip activateParams check.abi bool defaultNeedsActivation; - // These parameters need to be verified so the owner of a license can link to a parent ipId/policy + /// These parameters need to be verified so the owner of a license can link to a parent ipId/policy Parameter[] linkParentParams; + /// URL to the file containing the legal text for the license agreement string licenseUrl; } @@ -40,17 +50,30 @@ library Licensing { string licenseUrl; } + /// A particular configuration of a Licensing Framework, setting (or not) values fo the licensing + /// terms (parameters) of the framework. + /// The lengths of the param value arrays must correspond to the Parameter[] of the framework. struct Policy { + /// Id of a Licensing Framework uint256 frameworkId; + /// Array with values for parameters verifying conditions to mint a license. Empty bytes for index if + /// this policy wants to use the default value for the paremeter. bytes[] mintingParamValues; + /// Array with values for parameters verifying conditions to activate a license. Empty bytes for index if + /// this policy wants to use the default value for the paremeter. bytes[] activationParamValues; - // must be set to true if policy will mint licenses without the need for activation + /// If false, minted licenses will start activated and verification of activationParams will be skipped bool needsActivation; + /// Array with values for parameters verifying conditions to link a license to a parent. Empty bytes for index if + /// this policy wants to use the default value for the paremeter. bytes[] linkParentParamValues; } + /// Data that define a License Agreement NFT struct License { + /// the id for the Policy this License will set to the desired derivative IP after being burned. uint256 policyId; + /// Ids for the licensors, meaning the Ip Ids of the parents of the derivative to be created address[] licensorIpIds; } } diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index af9d5b26a..2bf4bf2d2 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -10,12 +10,12 @@ import { ERC1155Burnable } from "@openzeppelin/contracts/token/ERC1155/extension import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; -import "forge-std/console2.sol"; // TODO: consider disabling operators/approvals on creation contract LicenseRegistry is ERC1155, ERC1155Burnable { using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; using Strings for *; mapping(uint256 => Licensing.Framework) private _frameworks; @@ -26,7 +26,8 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { uint256 private _totalPolicies; // DO NOT remove policies, that rugs derivatives and breaks ordering assumptions in set mapping(address => EnumerableSet.UintSet) private _policiesPerIpId; - mapping(address => address[]) private _ipParents; + mapping(address => EnumerableSet.AddressSet) private _ipIdParents; + mapping(bytes32 => uint256) private _hashedLicenses; mapping(uint256 => Licensing.License) private _licenses; @@ -44,7 +45,10 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { constructor(string memory uri) ERC1155(uri) {} - // Protocol available terms + /// Adds a license framework to Story Protocol. + /// Must be called by protocol admin + /// @param fwCreation parameters + /// @return frameworkId identifier for framework, starting in 1 function addLicenseFramework(Licensing.FrameworkCreationParams calldata fwCreation) external returns(uint256 frameworkId) { // check protocol auth if (bytes(fwCreation.licenseUrl).length == 0 || fwCreation.licenseUrl.equal("")) { @@ -53,7 +57,6 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { // Todo: check duplications ++_totalFrameworks; - console2.log("_totalFrameworks", _totalFrameworks); _frameworks[_totalFrameworks].licenseUrl = fwCreation.licenseUrl; _frameworks[_totalFrameworks].defaultNeedsActivation = fwCreation.defaultNeedsActivation; _setParamArray(_frameworks[_totalFrameworks], Licensing.ParamVerifierType.Minting, fwCreation.mintingParamVerifiers, fwCreation.mintingParamDefaultValues); @@ -64,6 +67,12 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { return _totalFrameworks; } + /// Convenience method to convert IParamVerifier[] + bytes[] into Parameter[], then stores it in a Framework storage ref + /// (Parameter[] can be in storage but not in calldata) + /// @param fw storage ref to framework + /// @param pvType ParamVerifierType, to know which parameters the arrays correspond to + /// @param paramVerifiers verifier array contracts + /// @param paramDefaultValues default values for the verifiers. Must be equal in length with paramVerifiers function _setParamArray( Licensing.Framework storage fw, Licensing.ParamVerifierType pvType, @@ -91,14 +100,26 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { } } + /// Gets total frameworks supported by LicenseRegistry function totalFrameworks() external view returns(uint256) { return _totalFrameworks; } - function framework(uint256 frameworkId) external view returns(Licensing.Framework memory framework) { - return _frameworks[frameworkId]; + /// Returns framework for id. Reverts if not found + function framework(uint256 frameworkId) public view returns(Licensing.Framework memory fw) { + fw = _frameworks[frameworkId]; + if (bytes(fw.licenseUrl).length == 0) { + revert Errors.LicenseRegistry__FrameworkNotFound(); + } + return fw; } + /// Convenience method to store data without repetition, assigning an id to it if new or reusing the existing one if already stored + /// @param data raw bytes, abi.encode() a value to be hashed + /// @param _hashToIds storage ref to the mapping of hash -> data id + /// @param existingIds amount of distinct data stored. + /// @return id new sequential id if new data, reused id if not new + /// @return isNew true if a new id was generated, signaling the value was stored in _hashToIds. False if id is reused and data was not stored function _addIdOrGetExisting(bytes memory data, mapping(bytes32 => uint256) storage _hashToIds, uint256 existingIds) private returns(uint256 id, bool isNew) { // We could just use the hash of the policy as id to save some gas, but the UX/DX of having huge random // numbers for ID is bad enough to justify the cost, plus we have accountability on current number of @@ -113,12 +134,17 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { return (id, true); } - // Per IP ID - // Sets available policies per IPID, returns policy Id. This is not a license. - function addPolicy(address ipId, Licensing.Policy calldata pol) external returns(uint256 policyId, uint256 indexOnIpId) { + /// Adds a policy to an ipId, which can be used to mint licenses, which are permissions for ipIds to be derivatives (children). + /// If an exact policy already existed, it will reuse the id. + /// Will revert if ipId already has the same policy + /// @param ipId to receive the policy + /// @param pol policy data + /// @return policyId if policy data was in the contract, policyId is reused, if it's new, id will be new. + /// @return indexOnIpId position of policy within the ipIds policy set + function addPolicy(address ipId, Licensing.Policy memory pol) public returns(uint256 policyId, uint256 indexOnIpId) { // check protocol auth - Licensing.Framework memory fw = _frameworks[pol.frameworkId]; - + Licensing.Framework memory fw = framework(pol.frameworkId); + // TODO: check if policy is compatible with existing or is allowed to add more (uint256 polId, bool isNew) = _addIdOrGetExisting(abi.encode(pol), _hashedPolicies, _totalPolicies); policyId = polId; if (isNew) { @@ -129,6 +155,11 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { return (policyId, _addPolictyId(ipId, policyId)); } + /// Adds a policy id to the ipId policy set + /// Will revert if policy set already has policyId + /// @param ipId the IP identifier + /// @param policyId id of the policy data + /// @return index of the policy added to the set function _addPolictyId(address ipId, uint256 policyId) internal returns(uint256 index) { EnumerableSet.UintSet storage policySet = _policiesPerIpId[ipId]; // TODO: check if policy is compatible with the others @@ -139,15 +170,21 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { return policySet.length() - 1; } - + /// Returns amount of distinct licensing policies in LicenseRegistry function totalPolicies() external view returns(uint256) { return _totalPolicies; } - function policy(uint256 policyId) external view returns(Licensing.Policy memory) { - return _policies[policyId]; + /// Gets policy data for policyId, reverts if not found + function policy(uint256 policyId) public view returns(Licensing.Policy memory pol) { + pol = _policies[policyId]; + if (pol.frameworkId == 0) { + revert Errors.LicenseRegistry__PolicyNotFound(); + } + return pol; } + /// Gets the policy set for an IpId /// @dev potentially expensive operation, use with care function policyIdsForIp(address ipId) external view returns(uint256[] memory policyIds) { return _policiesPerIpId[ipId].values(); @@ -169,21 +206,34 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { return _policies[_policiesPerIpId[ipId].at(index)]; } + /// Mints license NFTs representing a licensing policy granted by a set of ipIds (licensors). This NFT needs to be burned + /// in order to link a derivative IP with its parents. + /// If this is the first combination of policy and licensors, a new licenseId will be created (by incrementing prev totalLicenses). + /// If not, the license is fungible and an id will be reused. + /// The licensing terms that regulate creating new licenses will be verified to allow minting. + /// Reverts if caller is not authorized by licensors. + /// @param licenseData policy Id and licensors + /// @param amount of licenses to be minted. License NFT is fungible for same policy and same licensors + /// @param receiver of the License NFT(s). + /// @return licenseId of the NFT(s). function mintLicense(Licensing.License calldata licenseData, uint256 amount, address receiver) external returns(uint256 licenseId) { + uint256 policyId = licenseData.policyId; + for(uint256 i = 0; i < licenseData.licensorIpIds.length; i++) { + address licensor = licenseData.licensorIpIds[i]; + if(!_policiesPerIpId[licensor].contains(policyId)) { + revert Errors.LicenseRegistry__LicensorDoesntHaveThisPolicy(); + } + // TODO: check duplicates // TODO: check if licensors are valid IP Ids and if they have been tagged bad // TODO: check if licensor allowed sender to mint in their behalf } - uint256 polId = licenseData.policyId; - Licensing.Policy memory pol = _policies[polId]; - if (pol.frameworkId == 0 || pol.frameworkId > _totalFrameworks) { - revert Errors.LicenseRegistry__PolicyNotFound(); - } + Licensing.Policy memory pol = policy(policyId); // TODO: execute minting params to check if they are valid - (uint256 lId, bool isNew) = _addIdOrGetExisting(abi.encode(pol), _hashedLicenses, _totalLicenses); + (uint256 lId, bool isNew) = _addIdOrGetExisting(abi.encode(licenseData), _hashedLicenses, _totalLicenses); licenseId = lId; if (isNew) { _totalLicenses = licenseId; @@ -194,10 +244,70 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { return licenseId; } + /// Returns true if holder has positive balance for licenseId function isLicensee(uint256 licenseId, address holder) external view returns(bool) { return balanceOf(holder, licenseId) > 0; } + /// Relates an IP ID with its parents (licensors), by burning the License NFT the holder owns + /// License must be activated to succeed, reverts otherwise. + /// Licensing parameters related to linking IPAs must be verified in order to succeed, reverts otherwise. + /// The child IP ID will have the policy that the license represent added to it's own, if it's compatible with + /// existing child policies. + /// The child IP ID will be linked to the parent (if it wasn't before). + /// @param licenseId + /// @param childIpId + /// @param holder + function setParentPolicy(uint256 licenseId, address childIpId, address holder) + external + onlyLicensee(licenseId, holder) { + // TODO: auth + // TODO: check if license is activated + // TODO: check if childIpId exists and is owned by holder + Licensing.License memory licenseData = _licenses[licenseId]; + address[] memory parents = licenseData.licensorIpIds; + for (uint256 i=0; i < parents.length; i++) { + // TODO: check licensor exist + // TODO: check licensor part of a bad tag branch + } + Licensing.Policy memory policy = policy(licenseData.policyId); + // TODO: check linking conditions + + // Add policy to kid + addPolicy(childIpId, policy); + // Set parent + for (uint256 i=0; i < parents.length; i++) { + // We don't care if it was already a parent, because there might be a case such as: + // 1. IP2 is created from IP1 with L1(non commercial) + // 2. IP1 releases L2 with commercial terms, and IP2 wants permission to commercially exploit + // 3. IP2 gets L2, burns it to set commercial policy + address parent = parents[i]; + if (parent == childIpId) { + revert Errors.LicenseRegistry__ParentIdEqualThanChild(); + } + _ipIdParents[childIpId].add(parent); + // TODO: emit + } + + // Burn license + _burn(holder, licenseId, 1); + } + + /// Returns true if the child is derivative from the parent, by at least 1 policy. + function isParent(address parentIpId, address childIpId) external view returns(bool) { + return _ipIdParents[childIpId].contains(parentIpId); + } + + function parentIpIds(address ipId) external view returns(address[] memory) { + return _ipIdParents[ipId].values(); + } + + function totalParentsForIpId(address ipId) external view returns(uint256) { + return _ipIdParents[ipId].length(); + } + // TODO: activation method + // TODO: tokenUri from parameters, from a metadata resolver contract + } \ No newline at end of file diff --git a/test/foundry/LicenseRegistry.t.sol b/test/foundry/LicenseRegistry.t.sol index ad8a9d7a5..30497ddea 100644 --- a/test/foundry/LicenseRegistry.t.sol +++ b/test/foundry/LicenseRegistry.t.sol @@ -20,7 +20,7 @@ contract LicenseRegistryTest is Test { Licensing.FrameworkCreationParams fwParams; address public ipId1 = address(0x111); address public ipId2 = address(0x222); - address public licensee = address(0x101); + address public licenseHolder = address(0x101); // TODO: add parameter config for initial framework for 100% test modifier withFrameworkParams() { @@ -140,7 +140,7 @@ contract LicenseRegistryTest is Test { assertEq(registry.policyIdForIpAtIndex(ipId1, 1), 2, "policyIdForIpAtIndex not 2"); } - function test_LicenseRegistry_mintLicense() public withFrameworkParams { + function test_LicenseRegistry_mintLicense() public withFrameworkParams returns(uint256 licenseId) { Licensing.Policy memory policy = Licensing.Policy({ frameworkId: 1, mintingParamValues: new bytes[](1), @@ -152,17 +152,32 @@ contract LicenseRegistryTest is Test { policy.activationParamValues[0] = abi.encode("test"); policy.linkParentParamValues[0] = abi.encode("test"); (uint256 policyId, uint256 indexOnIpId) = registry.addPolicy(ipId1, policy); - + assertEq(policyId, 1); Licensing.License memory licenseData = Licensing.License({ policyId: policyId, licensorIpIds: new address[](1) }); - licenseData.licensorIpIds[0] = ipId2; + licenseData.licensorIpIds[0] = ipId1; - uint256 licenseId = registry.mintLicense(licenseData, 2, licensee); + licenseId = registry.mintLicense(licenseData, 2, licenseHolder); assertEq(licenseId, 1); - assertEq(registry.balanceOf(licensee, licenseId), 2); + assertEq(registry.balanceOf(licenseHolder, licenseId), 2); + return licenseId; } + function test_LicenseRegistry_setParentId() public { + // TODO: something cleaner than this + uint256 licenseId = test_LicenseRegistry_mintLicense(); + + registry.setParentPolicy(licenseId, ipId2, licenseHolder); + assertEq(registry.balanceOf(licenseHolder, licenseId), 1, "not burnt"); + assertEq(registry.isParent(ipId1, ipId2), true, "not parent"); + assertEq( + keccak256(abi.encode(registry.policyForIpAtIndex(ipId2, 0))), + keccak256(abi.encode(registry.policyForIpAtIndex(ipId1, 0))), + "policy not copied" + ); + + } } From f5677d4cf28aa5ece516128659e8161274aebd3a Mon Sep 17 00:00:00 2001 From: Raul Date: Fri, 19 Jan 2024 01:40:50 -0800 Subject: [PATCH 09/13] remove Counter script --- script/Counter.s.sol | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 script/Counter.s.sol diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index a88b9f292..000000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import { Script } from "forge-std/Script.sol"; - -contract CounterScript is Script { - function setUp() public {} - - function run() public { - vm.broadcast(); - } -} From cf6063fe40ed277940dabefd92a39bfca73d1d57 Mon Sep 17 00:00:00 2001 From: Raul Date: Fri, 19 Jan 2024 01:42:44 -0800 Subject: [PATCH 10/13] fix natspec --- contracts/registries/LicenseRegistry.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index 2bf4bf2d2..c09b5fca8 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -255,9 +255,9 @@ contract LicenseRegistry is ERC1155, ERC1155Burnable { /// The child IP ID will have the policy that the license represent added to it's own, if it's compatible with /// existing child policies. /// The child IP ID will be linked to the parent (if it wasn't before). - /// @param licenseId - /// @param childIpId - /// @param holder + /// @param licenseId license NFT to be burned + /// @param childIpId that will receive the policy defined by licenseId + /// @param holder of the license NFT function setParentPolicy(uint256 licenseId, address childIpId, address holder) external onlyLicensee(licenseId, holder) { From 3f4c2294316d2ed5cf624a975f0532e2c30108b7 Mon Sep 17 00:00:00 2001 From: Raul Date: Fri, 19 Jan 2024 12:04:38 -0800 Subject: [PATCH 11/13] comment fix --- contracts/lib/Licensing.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/lib/Licensing.sol b/contracts/lib/Licensing.sol index 63cede566..100af97cd 100644 --- a/contracts/lib/Licensing.sol +++ b/contracts/lib/Licensing.sol @@ -30,7 +30,9 @@ library Licensing { Parameter[] mintingParams; /// License may need to be activated before linking, these parameters must be verified to activate. Parameter[] activationParams; - /// If the framework defaults to not needing activation, this can be set to true to skip activateParams check.abi + /// The framework might have activation terms defined, but the default settings say they are disabled + /// (so defaultNeedsActivation should be true). If the policy doesn't change this, it means licenses + /// will be minted Active and can't be linked out of the box (if linkParentParams are true) bool defaultNeedsActivation; /// These parameters need to be verified so the owner of a license can link to a parent ipId/policy Parameter[] linkParentParams; From 1bc58e32402ee7428b94a2c96ae3f30cc37b5287 Mon Sep 17 00:00:00 2001 From: Raul Date: Fri, 19 Jan 2024 12:05:26 -0800 Subject: [PATCH 12/13] typo --- contracts/lib/Licensing.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/lib/Licensing.sol b/contracts/lib/Licensing.sol index 100af97cd..f978e6d76 100644 --- a/contracts/lib/Licensing.sol +++ b/contracts/lib/Licensing.sol @@ -52,7 +52,7 @@ library Licensing { string licenseUrl; } - /// A particular configuration of a Licensing Framework, setting (or not) values fo the licensing + /// A particular configuration of a Licensing Framework, setting (or not) values for the licensing /// terms (parameters) of the framework. /// The lengths of the param value arrays must correspond to the Parameter[] of the framework. struct Policy { From 8a5c2292cf0a9eab665ae2ca22c3f47cf3521853 Mon Sep 17 00:00:00 2001 From: Raul Date: Fri, 19 Jan 2024 12:10:47 -0800 Subject: [PATCH 13/13] removed ERC1155 burnable --- contracts/registries/LicenseRegistry.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/registries/LicenseRegistry.sol b/contracts/registries/LicenseRegistry.sol index c09b5fca8..0fc87b91d 100644 --- a/contracts/registries/LicenseRegistry.sol +++ b/contracts/registries/LicenseRegistry.sol @@ -6,13 +6,12 @@ import { Licensing } from "../lib/Licensing.sol"; import { IParamVerifier } from "../interfaces/licensing/IParamVerifier.sol"; import { Errors } from "../lib/Errors.sol"; import { ERC1155 } from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; -import { ERC1155Burnable } from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Burnable.sol"; import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; // TODO: consider disabling operators/approvals on creation -contract LicenseRegistry is ERC1155, ERC1155Burnable { +contract LicenseRegistry is ERC1155 { using EnumerableSet for EnumerableSet.UintSet; using EnumerableSet for EnumerableSet.AddressSet;