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

Add Royalty License Term Processor and E2E test (MVP release) #133

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
61 changes: 61 additions & 0 deletions contracts/modules/licensing/terms/RoyaltyTermsProcessor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.19;

import { BaseTermsProcessor } from "./BaseTermsProcessor.sol";
import { IRoyaltyDistributor } from "contracts/modules/royalties/IRoyaltyDistributor.sol";
import { IIPAccountRegistry } from "contracts/ip-accounts/IIPAccountRegistry.sol";
import { IRoyaltyProportionPolicy } from "contracts/modules/royalties/policies/IRoyaltyProportionPolicy.sol";
import { EmptyArray, LengthMismatch } from "contracts/errors/General.sol";

struct RoyaltyTermsConfig {
address payerNftContract;
uint256 payerTokenId;
address[] accounts;
uint32[] allocationPercentages;
bool isExecuted;
}


/// @title RoyaltyTermsProcessor
/// @notice Processor to set royalty split plan for Licensee.
/// the percentage of income from licensee will pay back to the licensor
contract RoyaltyTermsProcessor is BaseTermsProcessor {
IRoyaltyDistributor public immutable ROYALTY_DISTRIBUTOR;

constructor(address authorizedExecutor, address royaltyDistributor) BaseTermsProcessor(authorizedExecutor) {
ROYALTY_DISTRIBUTOR = IRoyaltyDistributor(royaltyDistributor);
}

/// @dev Parse license terms and translate license terms into Royalty split
function _executeTerms(bytes calldata data) internal virtual override returns (bytes memory newData) {
RoyaltyTermsConfig memory config = abi.decode(data, (RoyaltyTermsConfig));
if (config.accounts.length == 0) {
revert EmptyArray();
}

if (config.accounts.length != config.allocationPercentages.length) {
revert LengthMismatch();
}
IRoyaltyProportionPolicy.ProportionData memory policyData = IRoyaltyProportionPolicy.ProportionData({
accounts: config.accounts,
percentAllocations: config.allocationPercentages
});
ROYALTY_DISTRIBUTOR.updateDistribution(config.payerNftContract, config.payerTokenId, abi.encode(policyData));

RoyaltyTermsConfig memory newConfig = RoyaltyTermsConfig({
payerNftContract: config.payerNftContract,
payerTokenId: config.payerTokenId,
accounts: config.accounts,
allocationPercentages: config.allocationPercentages,
isExecuted: true
});

newData = abi.encode(newConfig);
}

/// @dev Return true if the terms exec executed with any errors.
function termsExecutedSuccessfully(bytes calldata data) external pure override returns (bool) {
RoyaltyTermsConfig memory config = abi.decode(data, (RoyaltyTermsConfig));
return config.isExecuted;
}
}
177 changes: 177 additions & 0 deletions test/foundry/integration/e2e.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// SPDX-License-Identifier: BUSDL-1.1
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import "contracts/ip-accounts/IIPAccount.sol";
import "contracts/ip-accounts/IPAccountImpl.sol";
import "contracts/ip-accounts/IPAccountRegistry.sol";
import "contracts/modules/royalties/RoyaltyDistributor.sol";
import "contracts/modules/royalties/RoyaltyNFT.sol";
import "contracts/modules/royalties/ISplitMain.sol";
import "contracts/modules/royalties/policies/MutableRoyaltyProportionPolicy.sol";
import "contracts/modules/licensing/terms/RoyaltyTermsProcessor.sol";
import '../utils/BaseTest.sol';
import '../mocks/MockLicensingModule.sol';
import '../mocks/MockTermsProcessor.sol';
import '../mocks/RightsManagerHarness.sol';
import "../mocks/MockERC721.sol";
import "../mocks/MockERC20.sol";
import "contracts/errors/General.sol";

contract E2ETest is BaseTest {
RoyaltyDistributor public royaltyDistributor;
IPAccountRegistry public registry;
IPAccountImpl public implementation;
RoyaltyNFT public royaltyNft;
ISplitMain public splitMain;
MutableRoyaltyProportionPolicy public mutablePolicy;
MockERC20 public mERC20;
RoyaltyTermsProcessor royaltyTermsProcessor;

function setUp() virtual override public {
string memory mainnetRpc;
try vm.envString("MAINNET_RPC_URL") returns (string memory rpcUrl) {
mainnetRpc = rpcUrl;
} catch {
mainnetRpc = "https://eth-mainnet.g.alchemy.com/v2/demo";
}
console.log(mainnetRpc);
uint256 mainnetFork = vm.createFork(mainnetRpc);
vm.selectFork(mainnetFork);
assertEq(vm.activeFork(), mainnetFork);
console.log(block.number);
// using the existing SplitMain
splitMain = ISplitMain(0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE);
royaltyNft = new RoyaltyNFT(address(splitMain));
implementation = new IPAccountImpl();
registry = new IPAccountRegistry(address(implementation));
royaltyDistributor = new RoyaltyDistributor(address(registry), address(royaltyNft));
mutablePolicy = new MutableRoyaltyProportionPolicy(address(royaltyNft));
mERC20 = new MockERC20("Royalty Token", "RTK", 18);
deployProcessors = false;
super.setUp();
royaltyTermsProcessor = new RoyaltyTermsProcessor(address(ipAssetRegistry), address(royaltyDistributor));
}

function test_e2e() public {
uint256 rootIpAssetId = ipAssetRegistry.createIPAsset(
IPAsset(1),
"rootIPAssetName",
"The root IP Asset description",
"http://root.ipasset.media.url",
alice,
0
);

uint256 ipAssetId = ipAssetRegistry.createIPAsset(
IPAsset(1),
"DerivativeIPAssetName",
"The derivative IP Asset description",
"http://derivative.ipasset.media.url",
bob,
rootIpAssetId
);

uint256 parentLicenseId = ipAssetRegistry.getLicenseIdByTokenId(rootIpAssetId, true);
address rootIpAccount = registry.createAccount(block.chainid, address(ipAssetRegistry), rootIpAssetId, "");
address derivativeIpAccount = registry.createAccount(block.chainid, address(ipAssetRegistry), ipAssetId, "");
// build Royalty Terms Config
address[] memory royaltyAccounts = new address[](2);
uint32[] memory royaltyPercentages = new uint32[](2);
royaltyAccounts[0] = rootIpAccount;
royaltyAccounts[1] = derivativeIpAccount;
royaltyPercentages[0] = 100000;
royaltyPercentages[1] = 900000;
RoyaltyTermsConfig memory termsConfig = RoyaltyTermsConfig({
payerNftContract: address(ipAssetRegistry),
payerTokenId: ipAssetId,
accounts: royaltyAccounts,
allocationPercentages : royaltyPercentages,
isExecuted: false
});

// Create license
vm.prank(alice);
uint256 licenseId = ipAssetRegistry.createLicense(
ipAssetId,
parentLicenseId,
alice,
"https://derivative.commercial.license.uri",
revoker,
true,
true,
IERC5218.TermsProcessorConfig({
processor: royaltyTermsProcessor,
data: abi.encode(termsConfig)
})
);
bool commercial = true;
(RightsManager.License memory license, address owner) = ipAssetRegistry.getLicense(licenseId);
assertEq(licenseId, 4);
assertEq(owner, alice);
assertEq(license.active, true);
assertEq(license.canSublicense, true);
assertEq(license.commercial, commercial);
assertEq(license.parentLicenseId, parentLicenseId);
assertEq(license.tokenId, ipAssetId);
assertEq(license.revoker, revoker);
assertEq(license.uri, "https://derivative.commercial.license.uri");
assertEq(address(license.termsProcessor), address(royaltyTermsProcessor));
assertEq(license.termsData, abi.encode(termsConfig));
assertEq(licenseRegistry.ownerOf(licenseId), alice);

royaltyDistributor.setRoyaltyPolicy(address(ipAssetRegistry), ipAssetId, address(mutablePolicy), "");

// execute license terms
vm.startPrank(alice);
ipAssetRegistry.executeTerms(licenseId);
licenseRegistry.transferFrom(alice, bob, licenseId);
vm.stopPrank();

assertEq(ipAssetRegistry.isLicenseActive(licenseId), true);
assertEq(royaltyNft.balanceOf(rootIpAccount, royaltyNft.toTokenId(derivativeIpAccount)), 100000);
assertEq(royaltyNft.balanceOf(derivativeIpAccount, royaltyNft.toTokenId(derivativeIpAccount)), 900000);

mERC20.mint(10000);
mERC20.transfer(derivativeIpAccount, 10000);
assertEq(mERC20.balanceOf(derivativeIpAccount), 10000);
vm.startPrank(derivativeIpAccount);
mERC20.approve(address(royaltyNft), 10000);
vm.stopPrank();
royaltyDistributor.distribute(address(ipAssetRegistry), ipAssetId, address(mERC20));
// splitMain always reserve 1 as minimal balance
assertEq(splitMain.getERC20Balance(rootIpAccount, mERC20), 999);
assertEq(splitMain.getERC20Balance(derivativeIpAccount, mERC20), 8999);
assertEq(mERC20.balanceOf(rootIpAccount), 0);
assertEq(mERC20.balanceOf(derivativeIpAccount), 0);

royaltyDistributor.claim(rootIpAccount, address(mERC20));
assertEq(mERC20.balanceOf(rootIpAccount), 998);
royaltyDistributor.claim(derivativeIpAccount, address(mERC20));
assertEq(mERC20.balanceOf(derivativeIpAccount),8998);
}

function _getLicensingConfig() view internal virtual override returns (ILicensingModule.FranchiseConfig memory) {
return ILicensingModule.FranchiseConfig({
nonCommercialConfig: ILicensingModule.IpAssetConfig({
canSublicense: true,
franchiseRootLicenseId: 0
}),
nonCommercialTerms: IERC5218.TermsProcessorConfig({
processor: nonCommercialTermsProcessor,
data: abi.encode("nonCommercial")
}),
commercialConfig: ILicensingModule.IpAssetConfig({
canSublicense: true,
franchiseRootLicenseId: 0
}),
commercialTerms: IERC5218.TermsProcessorConfig({
processor: commercialTermsProcessor,
data: abi.encode("commercial")
}),
rootIpAssetHasCommercialRights: true,
revoker: revoker,
commercialLicenseUri: "uriuri"
});
}
}
101 changes: 101 additions & 0 deletions test/foundry/licensing/terms/RoyaltyTermsProcessor.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: BUSDL-1.1
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import 'test/foundry/utils/BaseTest.sol';
import "contracts/errors/General.sol";
import "contracts/modules/licensing/terms/RoyaltyTermsProcessor.sol";
import "test/foundry/mocks/MockRoyaltyDistributor.sol";

contract RoyaltyTermsProcessorTest is BaseTest {
address licenseHolder = address(0x888888);
RoyaltyTermsProcessor processor;
uint256 licenseId;
uint256 ipAssetId;
uint256 parentLicenseId;
MockRoyaltyDistributor royaltyDistributor;

address[] accounts;
uint32[] allocationPercentages;


function setUp() virtual override public {
deployProcessors = false;
super.setUp();
royaltyDistributor = new MockRoyaltyDistributor();
ipAssetId = ipAssetRegistry.createIPAsset(IPAsset(1), "name", "description", "mediaUrl", licenseHolder, 0);
parentLicenseId = ipAssetRegistry.getLicenseIdByTokenId(ipAssetId, false);
processor = new RoyaltyTermsProcessor(address(ipAssetRegistry), address(royaltyDistributor));
}

function test_revert_execute_terms_unauthorized() public {
bytes memory data = abi.encode(1);
vm.expectRevert(Unauthorized.selector);
processor.executeTerms(data);
}

function test_revert_execute_terms_empty_accounts() public {
RoyaltyTermsConfig memory termsConfig = RoyaltyTermsConfig({
payerNftContract: address(ipAssetRegistry),
payerTokenId: ipAssetId,
accounts: accounts,
allocationPercentages : allocationPercentages,
isExecuted: false
});
vm.prank(address(ipAssetRegistry));
vm.expectRevert(EmptyArray.selector);
processor.executeTerms(abi.encode(termsConfig));
}

function test_revert_execute_terms_mismatch_accounts_allocations() public {
accounts = [address(0x888888)];
allocationPercentages = [100000, 900000];
RoyaltyTermsConfig memory termsConfig = RoyaltyTermsConfig({
payerNftContract: address(ipAssetRegistry),
payerTokenId: ipAssetId,
accounts: accounts,
allocationPercentages : allocationPercentages,
isExecuted: false
});
vm.prank(address(ipAssetRegistry));
vm.expectRevert(LengthMismatch.selector);
processor.executeTerms(abi.encode(termsConfig));
}

function test_execute_terms_start_on_license_creation() public {
accounts = [address(0x777777), address(0x888888)];
allocationPercentages = [100000, 900000];
RoyaltyTermsConfig memory config = RoyaltyTermsConfig({
payerNftContract: address(ipAssetRegistry),
payerTokenId: ipAssetId,
accounts: accounts,
allocationPercentages : allocationPercentages,
isExecuted: false
});

IERC5218.TermsProcessorConfig memory termsConfig = IERC5218.TermsProcessorConfig({
processor: processor,
data: abi.encode(config)
});

assertFalse(processor.termsExecutedSuccessfully(abi.encode(config)), "terms should be inactive before start time");

vm.prank(licenseHolder);
licenseId = ipAssetRegistry.createLicense(
ipAssetId,
parentLicenseId,
licenseHolder,
"licenseUri",
revoker,
false,
false,
termsConfig
);
vm.prank(licenseHolder);
ipAssetRegistry.executeTerms(licenseId);
assertTrue(ipAssetRegistry.isLicenseActive(licenseId), "execution terms should make license active");
(RightsManager.License memory license, address owner) = ipAssetRegistry.getLicense(licenseId);
assertEq(owner, licenseHolder);
assertTrue(processor.termsExecutedSuccessfully(license.termsData), "execution terms should executed successfully.");
}
}
49 changes: 49 additions & 0 deletions test/foundry/mocks/MockRoyaltyDistributor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.18;

import "contracts/modules/royalties/IRoyaltyDistributor.sol";
import { CollectModuleBase } from "contracts/modules/collect/CollectModuleBase.sol";

/// @title Mock Royalty Distributor
/// @notice This mock contract is used for testing the Royalty Terms Processor.
contract MockRoyaltyDistributor is IRoyaltyDistributor {
address public defaultRoyaltyPolicy;
/// @notice Set royalty policy to specified IP Asset.
/// @param royaltyPolicy The royalty distribution policy.
function setRoyaltyPolicy(
address,
uint256,
address royaltyPolicy,
bytes calldata
) external {
defaultRoyaltyPolicy = royaltyPolicy;
}

/// @notice Get royalty policy for specified IP Asset.
/// @return The address of royalty distribution policy.
function getRoyaltyPolicy(address, uint256) external view returns (address) {
return defaultRoyaltyPolicy;
}

/// @notice update royalty distribution plan for given IP Asset.
/// @param nftContract address of NFT collection contract.
/// @param tokenId The NFT token Id of NFT collection contract.
/// @param data The royalty distribution plan data.
function updateDistribution(address nftContract, uint256 tokenId, bytes calldata data) external {}

/// @notice distribute royalty to each recipient according to royalty distribution plan for given IP Asset.
/// @param nftContract address of NFT collection contract.
/// @param tokenId The NFT token Id of NFT collection contract.
/// @param token The ERC20 token for royalty.
function distribute(address nftContract, uint256 tokenId, address token) external {}

/// @notice claim royalty to account.
/// @param account address of the account to which withdraw royalty which distributed before.
function claim(address account, address token) external {}

/// @notice pause the royalty distribution.
function pause() external {}

/// @notice unpause the royalty distribution.
function unpause() external {}
}
Loading