Skip to content

Commit

Permalink
Expiration Time for Child IPs Not Exceed Parent IP (storyprotocol#129)
Browse files Browse the repository at this point in the history
* childIP expire time cannot longer than parent IP expire time
* reduce complexity
* extract a common lib function for earliest expiration time
  • Loading branch information
kingster-will authored Apr 22, 2024
1 parent 888bb8b commit 96ebc7c
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 12 deletions.
15 changes: 15 additions & 0 deletions contracts/lib/ExpiringOps.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.23;

library ExpiringOps {
/// @dev Get the earliest expiration time from two expiration times
/// @param currentEarliestExp The current earliest expiration time
/// @param anotherExp Another expiration time
function getEarliestExpirationTime(
uint256 currentEarliestExp,
uint256 anotherExp
) internal view returns (uint256 earliestExp) {
earliestExp = currentEarliestExp;
if (anotherExp > 0 && (anotherExp < earliestExp || earliestExp == 0)) earliestExp = anotherExp;
}
}
6 changes: 2 additions & 4 deletions contracts/modules/licensing/PILicenseTemplate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { IHookModule } from "../../interfaces/modules/base/IHookModule.sol";
import { ILicenseRegistry } from "../../interfaces/registries/ILicenseRegistry.sol";
import { IRoyaltyModule } from "../../interfaces/modules/royalty/IRoyaltyModule.sol";
import { PILicenseTemplateErrors } from "../../lib/PILicenseTemplateErrors.sol";
import { ExpiringOps } from "../../lib/ExpiringOps.sol";
import { IPILicenseTemplate, PILTerms } from "../../interfaces/modules/licensing/IPILicenseTemplate.sol";
import { BaseLicenseTemplateUpgradeable } from "../../modules/licensing/BaseLicenseTemplateUpgradeable.sol";
import { LicensorApprovalChecker } from "../../modules/licensing/parameter-helpers/LicensorApprovalChecker.sol";
Expand Down Expand Up @@ -244,10 +245,7 @@ contract PILicenseTemplate is
}
uint expireTime = _getExpireTime(licenseTermsIds[0], start);
for (uint i = 1; i < licenseTermsIds.length; i++) {
uint newExpireTime = _getExpireTime(licenseTermsIds[i], start);
if (newExpireTime < expireTime || expireTime == 0) {
expireTime = newExpireTime;
}
expireTime = ExpiringOps.getEarliestExpirationTime(expireTime, _getExpireTime(licenseTermsIds[i], start));
}
return expireTime;
}
Expand Down
58 changes: 50 additions & 8 deletions contracts/registries/LicenseRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ILicensingModule } from "../interfaces/modules/licensing/ILicensingModu
import { IDisputeModule } from "../interfaces/modules/dispute/IDisputeModule.sol";
import { Errors } from "../lib/Errors.sol";
import { Licensing } from "../lib/Licensing.sol";
import { ExpiringOps } from "../lib/ExpiringOps.sol";
import { ILicenseTemplate } from "../interfaces/modules/licensing/ILicenseTemplate.sol";
import { IPAccountStorageOps } from "../lib/IPAccountStorageOps.sol";
import { IIPAccount } from "../interfaces/IIPAccount.sol";
Expand Down Expand Up @@ -219,8 +220,10 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr
if ($.attachedLicenseTerms[childIpId].length() > 0) {
revert Errors.LicenseRegistry__DerivativeIpAlreadyHasLicense(childIpId);
}

// earliest expiration time
uint256 earliestExp = 0;
for (uint256 i = 0; i < parentIpIds.length; i++) {
earliestExp = ExpiringOps.getEarliestExpirationTime(earliestExp, _getExpireTime(parentIpIds[i]));
_verifyDerivativeFromParent(parentIpIds[i], childIpId, licenseTemplate, licenseTermsIds[i]);
$.childIps[parentIpIds[i]].add(childIpId);
// determine if duplicate license terms
Expand All @@ -230,12 +233,11 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr
revert Errors.LicenseRegistry__DuplicateLicense(parentIpIds[i], licenseTemplate, licenseTermsIds[i]);
}
}

$.licenseTemplates[childIpId] = licenseTemplate;
_setExpirationTime(
childIpId,
ILicenseTemplate(licenseTemplate).getEarlierExpireTime(licenseTermsIds, block.timestamp)
);
// calculate the earliest expiration time of child IP with both parent IPs and license terms
earliestExp = _calculateEarliestExpireTime(earliestExp, licenseTemplate, licenseTermsIds);
// default value is 0 which means that the license never expires
if (earliestExp != 0) _setExpirationTime(childIpId, earliestExp);
}

/// @notice Verifies the minting of a license token.
Expand Down Expand Up @@ -390,7 +392,7 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr
/// @param ipId The address of the IP.
/// @return The expiration time, 0 means never expired.
function getExpireTime(address ipId) external view returns (uint256) {
return IIPAccount(payable(ipId)).getUint256(EXPIRATION_TIME);
return _getExpireTime(ipId);
}

/// @notice Checks if an IP is expired.
Expand Down Expand Up @@ -438,22 +440,51 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr
}
}

/// @dev Calculate the earliest expiration time of the child IP with both parent IPs and license terms
/// @param earliestParentIpExp The earliest expiration time of among all parent IPs
/// @param licenseTemplate The address of the license template where the license terms are created
/// @param licenseTermsIds The license terms the child IP is registered with
function _calculateEarliestExpireTime(
uint256 earliestParentIpExp,
address licenseTemplate,
uint256[] calldata licenseTermsIds
) internal view returns (uint256 earliestExp) {
uint256 licenseExp = ILicenseTemplate(licenseTemplate).getEarlierExpireTime(licenseTermsIds, block.timestamp);
earliestExp = ExpiringOps.getEarliestExpirationTime(earliestParentIpExp, licenseExp);
}

/// @dev Get the expiration time of an IP
/// @param ipId The address of the IP
function _getExpireTime(address ipId) internal view returns (uint256) {
return IIPAccount(payable(ipId)).getUint256(EXPIRATION_TIME);
}

/// @dev Check if an IP is expired now
/// @param ipId The address of the IP
function _isExpiredNow(address ipId) internal view returns (bool) {
uint256 expireTime = IIPAccount(payable(ipId)).getUint256(EXPIRATION_TIME);
uint256 expireTime = _getExpireTime(ipId);
return expireTime != 0 && expireTime < block.timestamp;
}

/// @dev Set the expiration time of an IP
/// @param ipId The address of the IP
/// @param expireTime The expiration time
function _setExpirationTime(address ipId, uint256 expireTime) internal {
IIPAccount(payable(ipId)).setUint256(EXPIRATION_TIME, expireTime);
emit ExpirationTimeSet(ipId, expireTime);
}

/// @dev Check if an IP is a derivative/child IP
/// @param childIpId The address of the IP
function _isDerivativeIp(address childIpId) internal view returns (bool) {
return _getLicenseRegistryStorage().parentIps[childIpId].length() > 0;
}

/// @dev Retrieves the minting license configuration for a given license terms of the IP.
/// Will return the configuration for the license terms of the IP if configuration is not set for the license terms.
/// @param ipId The address of the IP.
/// @param licenseTemplate The address of the license template where the license terms are defined.
/// @param licenseTermsId The ID of the license terms.
function _getMintingLicenseConfig(
address ipId,
address licenseTemplate,
Expand All @@ -469,6 +500,10 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr
return $.mintingLicenseConfigsForIp[ipId];
}

/// @dev Get the hash of the IP ID, license template, and license terms ID
/// @param ipId The address of the IP
/// @param licenseTemplate The address of the license template
/// @param licenseTermsId The ID of the license terms
function _getIpLicenseHash(
address ipId,
address licenseTemplate,
Expand All @@ -477,6 +512,10 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr
return keccak256(abi.encode(ipId, licenseTemplate, licenseTermsId));
}

/// @dev Check if an IP has attached given license terms
/// @param ipId The address of the IP
/// @param licenseTemplate The address of the license template
/// @param licenseTermsId The ID of the license terms
function _hasIpAttachedLicenseTerms(
address ipId,
address licenseTemplate,
Expand All @@ -487,6 +526,9 @@ contract LicenseRegistry is ILicenseRegistry, AccessManagedUpgradeable, UUPSUpgr
return $.licenseTemplates[ipId] == licenseTemplate && $.attachedLicenseTerms[ipId].contains(licenseTermsId);
}

/// @dev Check if license terms has been defined in the license template
/// @param licenseTemplate The address of the license template
/// @param licenseTermsId The ID of the license terms
function _exists(address licenseTemplate, uint256 licenseTermsId) internal view returns (bool) {
if (!_getLicenseRegistryStorage().registeredLicenseTemplates[licenseTemplate]) {
return false;
Expand Down
75 changes: 75 additions & 0 deletions test/foundry/registries/LicenseRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { PILFlavors } from "../../../contracts/lib/PILFlavors.sol";
import { MockLicenseTemplate } from "../mocks/module/MockLicenseTemplate.sol";
import { IPAccountStorageOps } from "../../../contracts/lib/IPAccountStorageOps.sol";
import { Licensing } from "../../../contracts/lib/Licensing.sol";
import { PILTerms } from "../../../contracts/interfaces/modules/licensing/IPILicenseTemplate.sol";

// test
import { MockERC721 } from "../mocks/token/MockERC721.sol";
Expand Down Expand Up @@ -310,6 +311,80 @@ contract LicenseRegistryTest is BaseTest {
});
}

function test_LicenseRegistry_registerDerivativeIp_parentIpExpireFirst() public {
vm.prank(address(licensingModule));
licenseRegistry.setExpireTime(ipAcct[1], block.timestamp + 100);
PILTerms memory terms1 = PILFlavors.nonCommercialSocialRemixing();
terms1.expiration = 200;
uint256 termsId1 = pilTemplate.registerLicenseTerms(terms1);
vm.prank(ipOwner[1]);
licensingModule.attachLicenseTerms(ipAcct[1], address(pilTemplate), termsId1);

PILTerms memory terms2 = PILFlavors.nonCommercialSocialRemixing();
terms2.expiration = 400;
uint256 termsId2 = pilTemplate.registerLicenseTerms(terms2);
vm.prank(ipOwner[2]);
licensingModule.attachLicenseTerms(ipAcct[2], address(pilTemplate), termsId2);

address[] memory parentIpIds = new address[](2);
uint256[] memory licenseTermsIds = new uint256[](2);
parentIpIds[0] = ipAcct[1];
parentIpIds[1] = ipAcct[2];
licenseTermsIds[0] = termsId1;
licenseTermsIds[1] = termsId2;
vm.prank(address(licensingModule));
licenseRegistry.registerDerivativeIp(ipAcct[3], parentIpIds, address(pilTemplate), licenseTermsIds);

assertEq(licenseRegistry.getExpireTime(ipAcct[1]), block.timestamp + 100, "ipAcct[1] expire time is incorrect");
assertEq(licenseRegistry.getExpireTime(ipAcct[2]), 0, "ipAcct[2] expire time is incorrect");
assertEq(licenseRegistry.getExpireTime(ipAcct[3]), block.timestamp + 100, "ipAcct[3] expire time is incorrect");
assertEq(licenseRegistry.getParentIp(ipAcct[3], 0), ipAcct[1]);
assertEq(licenseRegistry.getParentIp(ipAcct[3], 1), ipAcct[2]);
(address attachedTemplate1, uint256 attachedTermsId1) = licenseRegistry.getAttachedLicenseTerms(ipAcct[3], 0);
assertEq(attachedTemplate1, address(pilTemplate));
assertEq(attachedTermsId1, termsId1);
(address attachedTemplate2, uint256 attachedTermsId2) = licenseRegistry.getAttachedLicenseTerms(ipAcct[3], 1);
assertEq(attachedTemplate2, address(pilTemplate));
assertEq(attachedTermsId2, termsId2);
}

function test_LicenseRegistry_registerDerivativeIp_termsExpireFirst() public {
vm.prank(address(licensingModule));
licenseRegistry.setExpireTime(ipAcct[1], block.timestamp + 500);
PILTerms memory terms1 = PILFlavors.nonCommercialSocialRemixing();
terms1.expiration = 0;
uint256 termsId1 = pilTemplate.registerLicenseTerms(terms1);
vm.prank(ipOwner[1]);
licensingModule.attachLicenseTerms(ipAcct[1], address(pilTemplate), termsId1);

PILTerms memory terms2 = PILFlavors.nonCommercialSocialRemixing();
terms2.expiration = 400;
uint256 termsId2 = pilTemplate.registerLicenseTerms(terms2);
vm.prank(ipOwner[2]);
licensingModule.attachLicenseTerms(ipAcct[2], address(pilTemplate), termsId2);

address[] memory parentIpIds = new address[](2);
uint256[] memory licenseTermsIds = new uint256[](2);
parentIpIds[0] = ipAcct[1];
parentIpIds[1] = ipAcct[2];
licenseTermsIds[0] = termsId1;
licenseTermsIds[1] = termsId2;
vm.prank(address(licensingModule));
licenseRegistry.registerDerivativeIp(ipAcct[3], parentIpIds, address(pilTemplate), licenseTermsIds);

assertEq(licenseRegistry.getExpireTime(ipAcct[1]), block.timestamp + 500, "ipAcct[1] expire time is incorrect");
assertEq(licenseRegistry.getExpireTime(ipAcct[2]), 0, "ipAcct[2] expire time is incorrect");
assertEq(licenseRegistry.getExpireTime(ipAcct[3]), block.timestamp + 400, "ipAcct[3] expire time is incorrect");
assertEq(licenseRegistry.getParentIp(ipAcct[3], 0), ipAcct[1]);
assertEq(licenseRegistry.getParentIp(ipAcct[3], 1), ipAcct[2]);
(address attachedTemplate1, uint256 attachedTermsId1) = licenseRegistry.getAttachedLicenseTerms(ipAcct[3], 0);
assertEq(attachedTemplate1, address(pilTemplate));
assertEq(attachedTermsId1, termsId1);
(address attachedTemplate2, uint256 attachedTermsId2) = licenseRegistry.getAttachedLicenseTerms(ipAcct[3], 1);
assertEq(attachedTemplate2, address(pilTemplate));
assertEq(attachedTermsId2, termsId2);
}

function onERC721Received(address, address, uint256, bytes memory) public pure returns (bytes4) {
return this.onERC721Received.selector;
}
Expand Down

0 comments on commit 96ebc7c

Please sign in to comment.