Skip to content

Commit

Permalink
Add Royalty Term Processor and E2E test (#133)
Browse files Browse the repository at this point in the history
  • Loading branch information
kingster-will authored Oct 19, 2023
1 parent 1d52be9 commit 2aabf9f
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 1 deletion.
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

0 comments on commit 2aabf9f

Please sign in to comment.