Skip to content

Commit

Permalink
Unit Tests and Enhancements for Licensing Components (storyprotocol#64)
Browse files Browse the repository at this point in the history
* Add licensing tests
* Add More tests
  • Loading branch information
kingster-will authored and Spablob committed Apr 12, 2024
1 parent 3c6de20 commit df15b09
Show file tree
Hide file tree
Showing 15 changed files with 2,492 additions and 325 deletions.
8 changes: 4 additions & 4 deletions contracts/LicenseToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag
/// @notice Emitted for metadata updates, per EIP-4906
event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId);

/// @dev Storage of the LicenseToken
/// @dev Storage structure for the LicenseToken
/// @custom:storage-location erc7201:story-protocol.LicenseToken
struct LicenseTokenStorage {
string imageUrl;
Expand All @@ -32,10 +32,9 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag
mapping(uint256 tokenId => LicenseTokenMetadata) licenseTokenMetadatas;
}

// TODO: update the storage location
// keccak256(abi.encode(uint256(keccak256("story-protocol.LicenseToken")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant LicenseTokenStorageLocation =
0x5ed898e10dedf257f39672a55146f3fecade9da16f4ff022557924a10d60a900;
0x62a0d75e37bea0c3e666dc72a74112fc6af15ce635719127e380d8ca1e555d00;

modifier onlyLicensingModule() {
if (msg.sender != address(_getLicenseTokenStorage().licensingModule)) {
Expand Down Expand Up @@ -319,7 +318,8 @@ contract LicenseToken is ILicenseToken, ERC721EnumerableUpgradeable, AccessManag
// Upgrades related //
////////////////////////////////////////////////////////////////////////////

function _getLicenseTokenStorage() internal pure returns (LicenseTokenStorage storage $) {
/// @dev Returns the storage struct of LicenseToken.
function _getLicenseTokenStorage() private pure returns (LicenseTokenStorage storage $) {
assembly {
$.slot := LicenseTokenStorageLocation
}
Expand Down
5 changes: 5 additions & 0 deletions contracts/interfaces/modules/licensing/IPILicenseTemplate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,9 @@ interface IPILicenseTemplate is ILicenseTemplate {
/// @param terms The PILTerms to get the ID for.
/// @return selectedLicenseTermsId The ID of the given license terms.
function getLicenseTermsId(PILTerms calldata terms) external view returns (uint256 selectedLicenseTermsId);

/// @notice Gets license terms of the given ID.
/// @param selectedLicenseTermsId The ID of the license terms.
/// @return terms The PILTerms associate with the given ID.
function getLicenseTerms(uint256 selectedLicenseTermsId) external view returns (PILTerms memory terms);
}
21 changes: 8 additions & 13 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,16 @@ library Errors {
error LicenseRegistry__ParentIpExpired(address ipId);
error LicenseRegistry__LicenseTermsNotExists(address licenseTemplate, uint256 licenseTermsId);
error LicenseRegistry__ParentIpHasNoLicenseTerms(address ipId, uint256 licenseTermsId);
error LicenseRegistry__LicensorIpHasNoLicenseTerms(address ipId, address licenseTemplate, uint256 licenseTermsId);
error LicenseRegistry__NoParentIp();
error LicenseRegistry__DerivativeIpAlreadyHasLicense(address childIpId);
error LicenseRegistry__DerivativeAlreadyRegistered(address childIpId);
error LicenseRegistry__ParentIpTagged(address ipId);
error LicenseRegistry__DerivativeIsParent(address ipId);
error LicenseRegistry__ParentIpUnmachedLicenseTemplate(address ipId, address licenseTemplate);
error LicenseRegistry__IndexOutOfBounds(address ipId, uint256 index, uint256 length);
error LicenseRegistry__LicenseTermsAlreadyAttached(address ipId, address licenseTemplate, uint256 licenseTermsId);
error LicenseRegistry__UnmatchedLicenseTemplate(address ipId, address licenseTemplate, address newLicenseTemplate);

////////////////////////////////////////////////////////////////////////////
// LicenseToken //
Expand Down Expand Up @@ -204,19 +207,11 @@ library Errors {
error LicensingModule__LicenseNotCompatibleForDerivative(address childIpId);
error LicensingModule__NoLicenseToken();
error LicensingModule__LicenseTokenNotCompatibleForDerivative(address childIpId, uint256[] licenseTokenIds);

////////////////////////////////////////////////////////////////////////////
// BasePolicyFrameworkManager //
////////////////////////////////////////////////////////////////////////////

error BasePolicyFrameworkManager__CallerNotLicensingModule();

////////////////////////////////////////////////////////////////////////////
// PolicyFrameworkManager //
////////////////////////////////////////////////////////////////////////////

error PolicyFrameworkManager__GettingPolicyWrongFramework();
error PolicyFrameworkManager__CommercializerCheckerDoesNotSupportHook(address commercializer);
error LicensingModule__LicenseDenyMintLicenseToken(
address licenseTemplate,
uint256 licenseTermsId,
address licensorIpId
);

////////////////////////////////////////////////////////////////////////////
// LicensorApprovalChecker //
Expand Down
30 changes: 18 additions & 12 deletions contracts/modules/licensing/BaseLicenseTemplateUpgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/I
import { ILicenseTemplate } from "../../interfaces/modules/licensing/ILicenseTemplate.sol";

abstract contract BaseLicenseTemplateUpgradeable is ILicenseTemplate, ERC165, Initializable {
/// @custom:storage-location erc7201:story-protocol.BaseLicenseTemplate
struct BaseLicenseTemplateStorage {
/// @dev Storage structure for the BaseLicenseTemplateUpgradeable
/// @custom:storage-location erc7201:story-protocol.BaseLicenseTemplateUpgradeable
struct BaseLicenseTemplateUpgradeableStorage {
string name;
string metadataURI;
}

// keccak256(abi.encode(uint256(keccak256("story-protocol.BaseLicenseTemplate")) - 1))
// & ~bytes32(uint256(0xff));
bytes32 private constant BaseLicenseTemplateStorageLocation =
0xa55803740ac9329334ad7b6cde0ec056cc3ba32125b59c579552512bed001f00;
// keccak256(abi.encode(uint256(keccak256("story-protocol.BaseLicenseTemplateUpgradeable")) - 1)) &
// ~bytes32(uint256(0xff));
bytes32 private constant BaseLicenseTemplateUpgradeableStorageLocation =
0x96c2f019b095cfe7c4d1f26aa9d2741961fe73294777688374a3299707c2fb00;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
Expand All @@ -29,28 +30,33 @@ abstract contract BaseLicenseTemplateUpgradeable is ILicenseTemplate, ERC165, In
/// @param _name The name of the license template
/// @param _metadataURI The URL to the off chain metadata
function __BaseLicenseTemplate_init(string memory _name, string memory _metadataURI) internal onlyInitializing {
_getBaseLicenseTemplateStorage().name = _name;
_getBaseLicenseTemplateStorage().metadataURI = _metadataURI;
_getBaseLicenseTemplateUpgradeableStorage().name = _name;
_getBaseLicenseTemplateUpgradeableStorage().metadataURI = _metadataURI;
}

/// @notice Returns the name of the license template
function name() public view override returns (string memory) {
return _getBaseLicenseTemplateStorage().name;
return _getBaseLicenseTemplateUpgradeableStorage().name;
}

/// @notice Returns the URL to the off chain metadata
function getMetadataURI() public view override returns (string memory) {
return _getBaseLicenseTemplateStorage().metadataURI;
return _getBaseLicenseTemplateUpgradeableStorage().metadataURI;
}

/// @notice IERC165 interface support.
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return interfaceId == type(ILicenseTemplate).interfaceId || super.supportsInterface(interfaceId);
}

function _getBaseLicenseTemplateStorage() internal pure returns (BaseLicenseTemplateStorage storage $) {
/// @dev Returns the storage struct of BaseLicenseTemplateUpgradeable.
function _getBaseLicenseTemplateUpgradeableStorage()
private
pure
returns (BaseLicenseTemplateUpgradeableStorage storage $)
{
assembly {
$.slot := BaseLicenseTemplateStorageLocation
$.slot := BaseLicenseTemplateUpgradeableStorageLocation
}
}
}
4 changes: 3 additions & 1 deletion contracts/modules/licensing/LicensingModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ contract LicensingModule is

_payMintingFee(licensorIpId, licenseTemplate, licenseTermsId, amount, royaltyContext, mlc);

ILicenseTemplate(licenseTemplate).verifyMintLicenseToken(licenseTermsId, receiver, licensorIpId, amount);
if (!ILicenseTemplate(licenseTemplate).verifyMintLicenseToken(licenseTermsId, receiver, licensorIpId, amount)) {
revert Errors.LicensingModule__LicenseDenyMintLicenseToken(licenseTemplate, licenseTermsId, licensorIpId);
}

startLicenseTokenId = LICENSE_NFT.mintLicenseTokens(
licensorIpId,
Expand Down
26 changes: 16 additions & 10 deletions contracts/modules/licensing/PILicenseTemplate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ contract PILicenseTemplate is
using ERC165Checker for address;
using Strings for *;

/// @dev Storage structure for the PILicenseTemplate
/// @custom:storage-location erc7201:story-protocol.PILicenseTemplate
struct PILicenseTemplateStorage {
mapping(uint256 licenseTermsId => PILTerms) licenseTerms;
Expand All @@ -39,20 +40,17 @@ contract PILicenseTemplate is
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
IRoyaltyModule public immutable ROYALTY_MODULE;

// TODO: update storage location
// keccak256(abi.encode(uint256(keccak256("story-protocol.BaseLicenseTemplate")) - 1))
// & ~bytes32(uint256(0xff));
// keccak256(abi.encode(uint256(keccak256("story-protocol.PILicenseTemplate")) - 1)) & ~bytes32(uint256(0xff));
bytes32 private constant PILicenseTemplateStorageLocation =
0xa55803740ac9329334ad7b6cde0ec056cc3ba32125b59c579552512bed001f00;
0xc6c6991297bc120d0383f0017fab72b8ca34fd4849ed6478dbaac67a33c3a700;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor(
address accessController,
address ipAccountRegistry,
address licenseRegistry,
address royaltyModule,
address licenseToken
) LicensorApprovalChecker(accessController, ipAccountRegistry, licenseToken) {
address royaltyModule
) LicensorApprovalChecker(accessController, ipAccountRegistry) {
LICENSE_REGISTRY = ILicenseRegistry(licenseRegistry);
ROYALTY_MODULE = IRoyaltyModule(royaltyModule);
_disableInitializers();
Expand Down Expand Up @@ -234,7 +232,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) {
if (newExpireTime < expireTime || expireTime == 0) {
expireTime = newExpireTime;
}
}
Expand All @@ -258,6 +256,14 @@ contract PILicenseTemplate is
return $.hashedLicenseTerms[licenseTermsHash];
}

/// @notice Gets license terms of the given ID.
/// @param selectedLicenseTermsId The ID of the license terms.
/// @return terms The PILTerms associate with the given ID.
function getLicenseTerms(uint256 selectedLicenseTermsId) external view returns (PILTerms memory terms) {
PILicenseTemplateStorage storage $ = _getPILicenseTemplateStorage();
return $.licenseTerms[selectedLicenseTermsId];
}

/// @notice Returns the total number of registered license terms.
/// @return The total number of registered license terms.
function totalRegisteredLicenseTerms() external view returns (uint256) {
Expand Down Expand Up @@ -289,7 +295,7 @@ contract PILicenseTemplate is
terms.expiration == 0 ? "never" : terms.expiration.toString(),
'"},',
'{"trait_type": "Currency", "value": "',
terms.currency == address(0) ? "Native Token" : terms.currency.toHexString(),
terms.currency.toHexString(),
'"},',
// Skip transferable, it's already added in the common attributes by the LicenseRegistry.
_policyCommercialTraitsToJson(terms),
Expand Down Expand Up @@ -419,7 +425,7 @@ contract PILicenseTemplate is

// If the policy defines the licensor must approve derivatives, check if the
// derivative is approved by the licensor
if (terms.derivativesApproval && !isDerivativeApproved(licenseTermsId, childIpId)) {
if (terms.derivativesApproval && !isDerivativeApproved(parentIpId, licenseTermsId, childIpId)) {
return false;
}
// Check if the commercializerChecker allows the link
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,19 @@
pragma solidity 0.8.23;

import { AccessControlled } from "../../../access/AccessControlled.sol";
import { ILicenseToken } from "../../../interfaces/ILicenseToken.sol";

import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

/// @title LicensorApprovalChecker
/// @notice Manages the approval of derivative IP accounts by the parentIp. Used to verify
/// licensing terms like "Derivatives With Approval" in PIL.
abstract contract LicensorApprovalChecker is AccessControlled, Initializable {
/// @notice Emits when a derivative IP account is approved by the parentIp.
/// @param licenseTokenId The ID of the license waiting for approval
/// @param licenseTermsId The ID of the license waiting for approval
/// @param ipId The ID of the derivative IP to be approved
/// @param caller The executor of the approval
/// @param approved Result of the approval
event DerivativeApproved(
uint256 indexed licenseTokenId,
uint256 indexed licenseTermsId,
address indexed ipId,
address indexed caller,
bool approved
Expand All @@ -30,10 +28,6 @@ abstract contract LicensorApprovalChecker is AccessControlled, Initializable {
mapping(uint256 => mapping(address => mapping(address => bool))) approvals;
}

/// @notice Returns the licenseToken address
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
ILicenseToken public immutable LICENSE_NFT;

// keccak256(abi.encode(uint256(keccak256("story-protocol.LicensorApprovalChecker")) - 1))
// & ~bytes32(uint256(0xff));
bytes32 private constant LicensorApprovalCheckerStorageLocation =
Expand All @@ -42,50 +36,49 @@ abstract contract LicensorApprovalChecker is AccessControlled, Initializable {
/// @notice Constructor function
/// @param accessController The address of the AccessController contract
/// @param ipAccountRegistry The address of the IPAccountRegistry contract
/// @param licenseToken The address of the LicenseRegistry contract
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(
address accessController,
address ipAccountRegistry,
address licenseToken
) AccessControlled(accessController, ipAccountRegistry) {
LICENSE_NFT = ILicenseToken(licenseToken);
}
address ipAccountRegistry
) AccessControlled(accessController, ipAccountRegistry) {}

/// @notice Approves or disapproves a derivative IP account.
/// @param licenseTokenId The ID of the license waiting for approval
/// @param parentIpId The ID of the parent IP grant the approval
/// @param licenseTermsId The ID of the license waiting for approval
/// @param childIpId The ID of the derivative IP to be approved
/// @param approved Result of the approval
function setApproval(uint256 licenseTokenId, address childIpId, bool approved) external {
address parentIpId = LICENSE_NFT.getLicensorIpId(licenseTokenId);
_setApproval(parentIpId, licenseTokenId, childIpId, approved);
function setApproval(address parentIpId, uint256 licenseTermsId, address childIpId, bool approved) external {
_setApproval(parentIpId, licenseTermsId, childIpId, approved);
}

/// @notice Checks if a derivative IP account is approved by the parent.
/// @param licenseTokenId The ID of the license NFT issued from a policy of the parent
/// @param licenseTermsId The ID of the license NFT issued from a policy of the parent
/// @param childIpId The ID of the derivative IP to be approved
/// @return approved True if the derivative IP account using the license is approved
function isDerivativeApproved(uint256 licenseTokenId, address childIpId) public view returns (bool) {
address parentIpId = LICENSE_NFT.getLicensorIpId(licenseTokenId);
function isDerivativeApproved(
address parentIpId,
uint256 licenseTermsId,
address childIpId
) public view returns (bool) {
LicensorApprovalCheckerStorage storage $ = _getLicensorApprovalCheckerStorage();
return $.approvals[licenseTokenId][parentIpId][childIpId];
return $.approvals[licenseTermsId][parentIpId][childIpId];
}

/// @notice Sets the approval for a derivative IP account.
/// @dev This function is only callable by the parent IP account.
/// @param parentIpId The ID of the parent IP account
/// @param licenseTokenId The ID of the license waiting for approval
/// @param licenseTermsId The ID of the license waiting for approval
/// @param childIpId The ID of the derivative IP to be approved
/// @param approved Result of the approval
function _setApproval(
address parentIpId,
uint256 licenseTokenId,
uint256 licenseTermsId,
address childIpId,
bool approved
) internal verifyPermission(parentIpId) {
LicensorApprovalCheckerStorage storage $ = _getLicensorApprovalCheckerStorage();
$.approvals[licenseTokenId][parentIpId][childIpId] = approved;
emit DerivativeApproved(licenseTokenId, parentIpId, msg.sender, approved);
$.approvals[licenseTermsId][parentIpId][childIpId] = approved;
emit DerivativeApproved(licenseTermsId, parentIpId, msg.sender, approved);
}

/// @dev Returns the storage struct of LicensorApprovalChecker.
Expand Down
Loading

0 comments on commit df15b09

Please sign in to comment.