Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Derivative Registration, IP Graph & Solady's ERC6551 Integration, and Update Protocol Packages #30

Merged
merged 7 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions contracts/SPGNFT.sol
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl
function mint(
address to,
string calldata nftMetadata
) public onlyRole(SPGNFTLib.MINTER_ROLE) returns (uint256 tokenId) {
) public virtual onlyRole(SPGNFTLib.MINTER_ROLE) returns (uint256 tokenId) {
tokenId = _mintToken({ to: to, payer: msg.sender, nftMetadata: nftMetadata });
}

Expand All @@ -130,7 +130,7 @@ contract SPGNFT is ISPGNFT, ERC721URIStorageUpgradeable, AccessControlUpgradeabl
address to,
address payer,
string calldata nftMetadata
) public onlySPG returns (uint256 tokenId) {
) public virtual onlySPG returns (uint256 tokenId) {
tokenId = _mintToken({ to: to, payer: payer, nftMetadata: nftMetadata });
}

Expand Down
44 changes: 27 additions & 17 deletions contracts/StoryProtocolGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.23;

import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
import { ERC721Holder } from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
// solhint-disable-next-line max-line-length
Expand Down Expand Up @@ -30,7 +31,7 @@ import { ISPGNFT } from "./interfaces/ISPGNFT.sol";
import { Errors } from "./lib/Errors.sol";
import { SPGNFTLib } from "./lib/SPGNFTLib.sol";

contract StoryProtocolGateway is IStoryProtocolGateway, AccessManagedUpgradeable, UUPSUpgradeable {
contract StoryProtocolGateway is IStoryProtocolGateway, ERC721Holder, AccessManagedUpgradeable, UUPSUpgradeable {
using ERC165Checker for address;
using SafeERC20 for IERC20;

Expand Down Expand Up @@ -336,8 +337,9 @@ contract StoryProtocolGateway is IStoryProtocolGateway, AccessManagedUpgradeable
});
}

/// @notice Mint an NFT from a collection and register it as a derivative IP using license tokens.
/// @dev Caller must have the minter role for the provided SPG NFT.
/// @notice Mint an NFT from a collection and register it as a derivative IP using license tokens
/// @dev Caller must have the minter role for the provided SPG NFT. Caller must own the license tokens and have
/// approved SPG to transfer them.
/// @param nftContract The address of the NFT collection.
/// @param licenseTokenIds The IDs of the license tokens to be burned for linking the IP to parent IPs.
/// @param royaltyContext The context for royalty module, should be empty for Royalty Policy LAP.
Expand All @@ -354,7 +356,7 @@ contract StoryProtocolGateway is IStoryProtocolGateway, AccessManagedUpgradeable
IPMetadata calldata ipMetadata,
address recipient
) external onlyCallerWithMinterRole(nftContract) returns (address ipId, uint256 tokenId) {
_transferLicenseTokens(licenseTokenIds);
_collectLicenseTokens(licenseTokenIds);

tokenId = ISPGNFT(nftContract).mintBySPG({ to: address(this), payer: msg.sender, nftMetadata: nftMetadata });
ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId);
Expand All @@ -366,6 +368,7 @@ contract StoryProtocolGateway is IStoryProtocolGateway, AccessManagedUpgradeable
}

/// @notice Register the given NFT as a derivative IP using license tokens.
/// @dev Caller must own the license tokens and have approved SPG to transfer them.
/// @param nftContract The address of the NFT collection.
/// @param tokenId The ID of the NFT.
/// @param licenseTokenIds The IDs of the license tokens to be burned for linking the IP to parent IPs.
Expand All @@ -383,6 +386,8 @@ contract StoryProtocolGateway is IStoryProtocolGateway, AccessManagedUpgradeable
SignatureData calldata sigMetadata,
SignatureData calldata sigRegister
) external returns (address ipId) {
_collectLicenseTokens(licenseTokenIds);

ipId = IP_ASSET_REGISTRY.register(block.chainid, nftContract, tokenId);
_setMetadataWithSig(ipMetadata, ipId, sigMetadata);
_setPermissionForModule(
Expand Down Expand Up @@ -411,16 +416,6 @@ contract StoryProtocolGateway is IStoryProtocolGateway, AccessManagedUpgradeable
LICENSING_MODULE.attachLicenseTerms(ipId, address(PIL_TEMPLATE), licenseTermsId);
}

/// @dev Transfers the license tokens from the caller to this contract.
/// @param licenseTokenIds The IDs of the license tokens to be transferred.
function _transferLicenseTokens(uint256[] calldata licenseTokenIds) internal {
if (licenseTokenIds.length == 0) revert Errors.SPG__EmptyLicenseTokens();
for (uint256 i = 0; i < licenseTokenIds.length; i++) {
if (LICENSE_TOKEN.ownerOf(licenseTokenIds[i]) == address(this)) continue;
LICENSE_TOKEN.transferFrom(msg.sender, address(this), licenseTokenIds[i]);
}
}

/// @dev Sets permission via signature to allow this contract to interact with the Licensing Module on behalf of the
/// provided IP Account.
/// @param ipId The ID of the IP.
Expand Down Expand Up @@ -484,6 +479,21 @@ contract StoryProtocolGateway is IStoryProtocolGateway, AccessManagedUpgradeable
_setMetadata(ipMetadata, ipId);
}

/// @dev Collects license tokens from the caller. Assumes SPG has permission to transfer the license tokens.
/// @param licenseTokenIds The IDs of the license tokens to be collected.
function _collectLicenseTokens(uint256[] calldata licenseTokenIds) internal {
if (licenseTokenIds.length == 0) revert Errors.SPG__EmptyLicenseTokens();
for (uint256 i = 0; i < licenseTokenIds.length; i++) {
address tokenOwner = LICENSE_TOKEN.ownerOf(licenseTokenIds[i]);

if (tokenOwner == address(this)) continue;
if (tokenOwner != address(msg.sender))
revert Errors.SPG__CallerAndNotTokenOwner(licenseTokenIds[i], msg.sender, tokenOwner);

LICENSE_TOKEN.safeTransferFrom(msg.sender, address(this), licenseTokenIds[i]);
}
}

/// @dev Collect mint fees for all parent IPs from the payer and set approval for Royalty Module to spend mint fees.
/// @param payerAddress The address of the payer for the license mint fees.
/// @param childIpId The ID of the derivative IP.
Expand All @@ -496,7 +506,7 @@ contract StoryProtocolGateway is IStoryProtocolGateway, AccessManagedUpgradeable
address[] calldata parentIpIds,
address licenseTemplate,
uint256[] calldata licenseTermsIds
) private {
) internal {
// Get currency token and royalty policy, assumes all parent IPs have the same currency token.
ILicenseTemplate lct = ILicenseTemplate(licenseTemplate);
(address royaltyPolicy, , , address mintFeeCurrencyToken) = lct.getRoyaltyPolicy(licenseTermsIds[0]);
Expand Down Expand Up @@ -526,7 +536,7 @@ contract StoryProtocolGateway is IStoryProtocolGateway, AccessManagedUpgradeable
address childIpId,
address licenseTemplate,
uint256[] calldata licenseTermsIds
) private returns (uint256 totalMintFee) {
) internal returns (uint256 totalMintFee) {
totalMintFee = 0;

for (uint256 i = 0; i < parentIpIds.length; i++) {
Expand All @@ -552,7 +562,7 @@ contract StoryProtocolGateway is IStoryProtocolGateway, AccessManagedUpgradeable
address licenseTemplate,
uint256 licenseTermsId,
uint256 amount
) private returns (uint256) {
) internal returns (uint256) {
ILicenseTemplate lct = ILicenseTemplate(licenseTemplate);

// Get mint fee set by license terms
Expand Down
3 changes: 3 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ library Errors {
/// @notice License token list is empty.
error SPG__EmptyLicenseTokens();

/// @notice License token is not owned by the either caller.
error SPG__CallerAndNotTokenOwner(uint256 tokenId, address caller, address actualTokenOwner);

/// @notice Zero address provided as a param.
error SPGNFT__ZeroAddressParam();

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@openzeppelin/contracts-upgradeable": "5.0.1",
"@story-protocol/create3-deployer": "github:storyprotocol/create3-deployer#main",
"@story-protocol/protocol-core": "github:storyprotocol/protocol-core-v1#main",
"erc6551": "^0.3.1",
"solady": "^0.0.192"
}
}
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@openzeppelin/=node_modules/@openzeppelin/
@storyprotocol/core/=node_modules/@story-protocol/protocol-core/contracts/
@create3-deployer/=node_modules/@story-protocol/create3-deployer/
@solady/=node_modules/solady/
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
2 changes: 2 additions & 0 deletions script/UpgradeSPG.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ contract UpgradeSPG is
accessControllerAddr,
ipAssetRegistryAddr,
licensingModuleAddr,
licenseRegistryAddr,
royaltyModuleAddr,
coreMetadataModuleAddr,
pilTemplateAddr,
licenseTokenAddr
Expand Down
2 changes: 1 addition & 1 deletion script/UpgradeSPGNFT.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ contract UpgradeSPGNFT is Script, StoryProtocolPeripheryAddressManager, Broadcas
using StringUtil for uint256;

ICreate3Deployer private constant create3Deployer = ICreate3Deployer(0x384a891dFDE8180b054f04D66379f16B7a678Ad6);
uint256 private constant create3SaltSeed = 15;
uint256 private constant create3SaltSeed = 12;

StoryProtocolGateway private spg;
SPGNFT private spgNftImpl;
Expand Down
61 changes: 41 additions & 20 deletions test/StoryProtocolGateway.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,12 @@ contract StoryProtocolGatewayTest is BaseTest {

uint256 deadline = block.timestamp + 1000;

(bytes memory sigMetadata, ) = _getSetPermissionSignatureForSPG({
(bytes memory sigMetadata, bytes32 expectedState, ) = _getSetPermissionSignatureForSPG({
ipId: expectedIpId,
module: address(coreMetadataModule),
selector: ICoreMetadataModule.setAll.selector,
deadline: deadline,
nonce: 1,
state: bytes32(0),
signerPk: alicePk
});

Expand All @@ -150,6 +150,7 @@ contract StoryProtocolGatewayTest is BaseTest {
sigMetadata: ISPG.SignatureData({ signer: alice, deadline: deadline, signature: sigMetadata })
});

assertEq(IIPAccount(payable(actualIpId)).state(), expectedState);
assertEq(actualIpId, expectedIpId);
assertTrue(ipAssetRegistry.isRegistered(actualIpId));
assertMetadata(actualIpId, ipMetadataDefault);
Expand All @@ -174,12 +175,12 @@ contract StoryProtocolGatewayTest is BaseTest {
address payable ipId = ipAsset[1].ipId;
uint256 deadline = block.timestamp + 1000;

(bytes memory signature, bytes memory data) = _getSetPermissionSignatureForSPG({
(bytes memory signature, , bytes memory data) = _getSetPermissionSignatureForSPG({
ipId: ipId,
module: address(licensingModule),
selector: ILicensingModule.attachLicenseTerms.selector,
deadline: deadline,
nonce: IIPAccount(ipId).state() + 1,
state: IIPAccount(ipId).state(),
signerPk: alicePk
});

Expand Down Expand Up @@ -253,21 +254,21 @@ contract StoryProtocolGatewayTest is BaseTest {

uint256 deadline = block.timestamp + 1000;

(bytes memory sigMetadata, ) = _getSetPermissionSignatureForSPG({
(bytes memory sigMetadata, bytes32 expectedState, ) = _getSetPermissionSignatureForSPG({
ipId: ipId,
module: address(coreMetadataModule),
selector: ICoreMetadataModule.setAll.selector,
deadline: deadline,
nonce: 1,
state: bytes32(0),
signerPk: alicePk
});

(bytes memory sigAttach, ) = _getSetPermissionSignatureForSPG({
(bytes memory sigAttach, , ) = _getSetPermissionSignatureForSPG({
ipId: ipId,
module: address(licensingModule),
selector: ILicensingModule.attachLicenseTerms.selector,
deadline: deadline,
nonce: 2,
state: expectedState,
signerPk: alicePk
});

Expand Down Expand Up @@ -433,21 +434,22 @@ contract StoryProtocolGatewayTest is BaseTest {

uint256[] memory licenseTokenIds = new uint256[](1);
licenseTokenIds[0] = startLicenseTokenId;
licenseToken.approve(address(spg), startLicenseTokenId);

(bytes memory sigMetadata, ) = _getSetPermissionSignatureForSPG({
(bytes memory sigMetadata, bytes32 expectedState, ) = _getSetPermissionSignatureForSPG({
ipId: ipIdChild,
module: address(coreMetadataModule),
selector: ICoreMetadataModule.setAll.selector,
deadline: deadline,
nonce: 1,
state: bytes32(0),
signerPk: alicePk
});
(bytes memory sigRegister, ) = _getSetPermissionSignatureForSPG({
(bytes memory sigRegister, , ) = _getSetPermissionSignatureForSPG({
ipId: ipIdChild,
module: address(licensingModule),
selector: ILicensingModule.registerDerivativeWithLicenseTokens.selector,
deadline: deadline,
nonce: 2,
state: expectedState,
signerPk: alicePk
});

Expand Down Expand Up @@ -509,17 +511,36 @@ contract StoryProtocolGatewayTest is BaseTest {
/// @param module The address of the module to set the permission for.
/// @param selector The selector of the function to be permitted for execution.
/// @param deadline The deadline for the signature.
/// @param nonce The IP's nonce for the signature.
/// @param state IPAccount's internal nonce
/// @param signerPk The private key of the signer.
/// @return signature The signature for setting the permission.
function _getSetPermissionSignatureForSPG(
address ipId,
address module,
bytes4 selector,
uint256 deadline,
uint256 nonce,
bytes32 state,
uint256 signerPk
) internal returns (bytes memory signature, bytes memory data) {
) internal returns (bytes memory signature, bytes32 expectedState, bytes memory data) {
expectedState = keccak256(
abi.encode(
state, // ipAccount.state()
abi.encodeWithSignature(
"execute(address,uint256,bytes)",
address(accessController),
0, // amount of ether to send
abi.encodeWithSignature(
"setPermission(address,address,address,bytes4,uint8)",
ipId,
address(spg),
address(module),
selector,
AccessPermission.ALLOW
)
)
)
);

data = abi.encodeWithSignature(
"setPermission(address,address,address,bytes4,uint8)",
ipId,
Expand All @@ -536,7 +557,7 @@ contract StoryProtocolGatewayTest is BaseTest {
to: address(accessController),
value: 0,
data: data,
nonce: nonce,
nonce: expectedState,
deadline: deadline
})
)
Expand Down Expand Up @@ -601,20 +622,20 @@ contract StoryProtocolGatewayTest is BaseTest {

uint256 deadline = block.timestamp + 1000;

(bytes memory sigMetadata, ) = _getSetPermissionSignatureForSPG({
(bytes memory sigMetadata, bytes32 expectedState, ) = _getSetPermissionSignatureForSPG({
ipId: ipIdChild,
module: address(coreMetadataModule),
selector: ICoreMetadataModule.setAll.selector,
deadline: deadline,
nonce: 1,
state: bytes32(0),
signerPk: alicePk
});
(bytes memory sigRegister, ) = _getSetPermissionSignatureForSPG({
(bytes memory sigRegister, , ) = _getSetPermissionSignatureForSPG({
ipId: ipIdChild,
module: address(licensingModule),
selector: ILicensingModule.registerDerivative.selector,
deadline: deadline,
nonce: 2,
state: expectedState,
signerPk: alicePk
});

Expand Down
Loading
Loading