Skip to content
This repository has been archived by the owner on Apr 30, 2024. It is now read-only.

Add licensing and royalty modules (initial thinking) #6

Merged
merged 13 commits into from
Jan 20, 2024
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:

- name: Run Forge tests
run: |
make test
forge test -vvv --fork-url https://eth.drpc.org --fork-block-number 18613489
id: forge-test

# - name: Gas Difference
Expand Down
31 changes: 31 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,37 @@ library Errors {
error LicenseRegistry__MintParamFailed();
error LicenseRegistry__LinkParentParamFailed();

////////////////////////////////////////////////////////////////////////////
// Dispute Module //
////////////////////////////////////////////////////////////////////////////

error DisputeModule__ZeroArbitrationPolicy();
error DisputeModule__ZeroArbitrationRelayer();
error DisputeModule__ZeroDisputeTag();
error DisputeModule__ZeroLinkToDisputeSummary();
error DisputeModule__NotWhitelistedArbitrationPolicy();
error DisputeModule__NotWhitelistedDisputeTag();
error DisputeModule__NotWhitelistedArbitrationRelayer();
error DisputeModule__NotDisputeInitiator();

error ArbitrationPolicySP__ZeroDisputeModule();
error ArbitrationPolicySP__ZeroPaymentToken();
error ArbitrationPolicySP__NotDisputeModule();

////////////////////////////////////////////////////////////////////////////
// Royalty Module //
////////////////////////////////////////////////////////////////////////////

error RoyaltyModule__ZeroRoyaltyPolicy();
error RoyaltyModule__NotWhitelistedRoyaltyPolicy();
error RoyaltyModule__AlreadySetRoyaltyPolicy();

error RoyaltyPolicyLS__ZeroRoyaltyModule();
error RoyaltyPolicyLS__ZeroLiquidSplitFactory();
error RoyaltyPolicyLS__ZeroLiquidSplitMain();
error RoyaltyPolicyLS__NotRoyaltyModule();
error RoyaltyPolicyLS__TransferFailed();

////////////////////////////////////////////////////////////////////////////
// ModuleRegistry //
////////////////////////////////////////////////////////////////////////////
Expand Down
174 changes: 174 additions & 0 deletions contracts/modules/dispute-module/DisputeModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

import {ShortStringEquals} from "../../utils/ShortStringOps.sol";
import {IArbitrationPolicy} from "../../../interfaces/modules/dispute-module/policies/IArbitrationPolicy.sol";
import {IDisputeModule} from "../../../interfaces/modules/dispute-module/IDisputeModule.sol";

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import {Errors} from "../../lib/Errors.sol";

/// @title Story Protocol Dispute Module
/// @notice The Story Protocol dispute module acts as an enforcement layer for
/// that allows to raise disputes and resolve them through arbitration.
contract DisputeModule is IDisputeModule, ReentrancyGuard {
/// @notice Dispute struct
struct Dispute {
address ipId; // The ipId
address disputeInitiator; // The address of the dispute initiator
address arbitrationPolicy; // The address of the arbitration policy
bytes32 linkToDisputeSummary; // The link of the dispute summary
bytes32 tag; // The target tag of the dispute // TODO: move to tagging module?
}

/// @notice Dispute id
uint256 public disputeId;

/// @notice Contains the dispute struct info for a given dispute id
mapping(uint256 disputeId => Dispute dispute) public disputes;

/// @notice Indicates if a dispute tag is whitelisted
mapping(bytes32 tag => bool allowed) public isWhitelistedDisputeTag;
Ramarti marked this conversation as resolved.
Show resolved Hide resolved

/// @notice Indicates if an arbitration policy is whitelisted
mapping(address arbitrationPolicy => bool allowed) public isWhitelistedArbitrationPolicy;

/// @notice Indicates if an arbitration relayer is whitelisted for a given arbitration policy
mapping(address arbitrationPolicy => mapping(address arbitrationRelayer => bool allowed)) public
Ramarti marked this conversation as resolved.
Show resolved Hide resolved
isWhitelistedArbitrationRelayer;

/// @notice Restricts the calls to the governance address
modifier onlyGovernance() {
LeoHChen marked this conversation as resolved.
Show resolved Hide resolved
// TODO: where is governance address defined?
_;
}

/// @notice Whitelists a dispute tag
/// @param _tag The dispute tag
/// @param _allowed Indicates if the dispute tag is whitelisted or not
function whitelistDisputeTags(bytes32 _tag, bool _allowed) external onlyGovernance {
if (_tag == bytes32(0)) revert Errors.DisputeModule__ZeroDisputeTag();

isWhitelistedDisputeTag[_tag] = _allowed;

// TODO: emit event
}

/// @notice Whitelists an arbitration policy
/// @param _arbitrationPolicy The address of the arbitration policy
/// @param _allowed Indicates if the arbitration policy is whitelisted or not
function whitelistArbitrationPolicy(address _arbitrationPolicy, bool _allowed) external onlyGovernance {
if (_arbitrationPolicy == address(0)) revert Errors.DisputeModule__ZeroArbitrationPolicy();

isWhitelistedArbitrationPolicy[_arbitrationPolicy] = _allowed;

// TODO: emit event
}

/// @notice Whitelists an arbitration relayer for a given arbitration policy
/// @param _arbitrationPolicy The address of the arbitration policy
/// @param _arbPolicyRelayer The address of the arbitration relayer
/// @param _allowed Indicates if the arbitration relayer is whitelisted or not
function whitelistArbitrationRelayer(address _arbitrationPolicy, address _arbPolicyRelayer, bool _allowed)
external
onlyGovernance
{
if (_arbitrationPolicy == address(0)) revert Errors.DisputeModule__ZeroArbitrationPolicy();
if (_arbPolicyRelayer == address(0)) revert Errors.DisputeModule__ZeroArbitrationRelayer();

isWhitelistedArbitrationRelayer[_arbitrationPolicy][_arbPolicyRelayer] = _allowed;

// TODO: emit event
}

/// @notice Raises a dispute
/// @param _ipId The ipId
/// @param _arbitrationPolicy The address of the arbitration policy
/// @param _linkToDisputeSummary The link of the dispute summary
/// @param _targetTag The target tag of the dispute
/// @param _data The data to initialize the policy
/// @return disputeId The dispute id
function raiseDispute(
address _ipId,
address _arbitrationPolicy,
string memory _linkToDisputeSummary,
bytes32 _targetTag,
bytes calldata _data
) external nonReentrant returns (uint256) {
// TODO: make call to ensure ipId exists/has been registered
if (!isWhitelistedArbitrationPolicy[_arbitrationPolicy]) {
revert Errors.DisputeModule__NotWhitelistedArbitrationPolicy();
}
if (!isWhitelistedDisputeTag[_targetTag]) revert Errors.DisputeModule__NotWhitelistedDisputeTag();

bytes32 linkToDisputeSummary = ShortStringEquals.stringToBytes32(_linkToDisputeSummary);
if (linkToDisputeSummary == bytes32(0)) revert Errors.DisputeModule__ZeroLinkToDisputeSummary();

disputeId++;

disputes[disputeId] = Dispute({
ipId: _ipId,
disputeInitiator: msg.sender,
arbitrationPolicy: _arbitrationPolicy,
linkToDisputeSummary: linkToDisputeSummary,
tag: _targetTag
});

// TODO: set tag to "in-dispute" state
LeoHChen marked this conversation as resolved.
Show resolved Hide resolved

IArbitrationPolicy(_arbitrationPolicy).onRaiseDispute(msg.sender, _data);

// TODO: emit event

return disputeId;
}

/// @notice Sets the dispute judgement
/// @param _disputeId The dispute id
/// @param _decision The decision of the dispute
/// @param _data The data to set the dispute judgement
function setDisputeJudgement(uint256 _disputeId, bool _decision, bytes calldata _data) external nonReentrant {
Ramarti marked this conversation as resolved.
Show resolved Hide resolved
address _arbitrationPolicy = disputes[_disputeId].arbitrationPolicy;

// TODO: if dispute tag is not in "in-dispute" state then the function should revert - the same disputeId cannot be set twice + cancelled cannot be set
if (!isWhitelistedArbitrationRelayer[_arbitrationPolicy][msg.sender]) {
revert Errors.DisputeModule__NotWhitelistedArbitrationRelayer();
}

if (_decision) {
// TODO: set tag to the target dispute tag state
} else {
// TODO: remove tag/set dispute tag to null state
}

IArbitrationPolicy(_arbitrationPolicy).onDisputeJudgement(_disputeId, _decision, _data);
Ramarti marked this conversation as resolved.
Show resolved Hide resolved

// TODO: emit event
}

/// @notice Cancels an ongoing dispute
/// @param _disputeId The dispute id
/// @param _data The data to cancel the dispute
function cancelDispute(uint256 _disputeId, bytes calldata _data) external nonReentrant {
if (msg.sender != disputes[_disputeId].disputeInitiator) revert Errors.DisputeModule__NotDisputeInitiator();
// TODO: if tag is not "in-dispute" then revert

IArbitrationPolicy(disputes[_disputeId].arbitrationPolicy).onDisputeCancel(msg.sender, _disputeId, _data);

// TODO: remove tag/set dispute tag to null state

// TODO: emit event
}

/// @notice Resolves a dispute after it has been judged
/// @param _disputeId The dispute id
function resolveDispute(uint256 _disputeId) external {
if (msg.sender != disputes[_disputeId].disputeInitiator) revert Errors.DisputeModule__NotDisputeInitiator();
// TODO: if tag is in "in-dispute" or already "null" then revert

// TODO: remove tag/set dispute tag to null state

// TODO: emit event
}
}
80 changes: 80 additions & 0 deletions contracts/modules/dispute-module/policies/ArbitrationPolicySP.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

import {IArbitrationPolicy} from "../../../../interfaces/modules/dispute-module/policies/IArbitrationPolicy.sol";
import {IDisputeModule} from "../../../../interfaces/modules/dispute-module/IDisputeModule.sol";

import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import {Errors} from "../../../lib/Errors.sol";

/// @title Story Protocol Arbitration Policy
/// @notice The Story Protocol arbitration policy is a simple policy that
/// requires the dispute initiator to pay a fixed amount of tokens
/// to raise a dispute and refunds that amount if the dispute initiator
/// wins the dispute.
contract ArbitrationPolicySP is IArbitrationPolicy {
using SafeERC20 for IERC20;

/// @notice Dispute module address
address public immutable DISPUTE_MODULE;

/// @notice Payment token address
address public immutable PAYMENT_TOKEN;

/// @notice Arbitration price
uint256 public immutable ARBITRATION_PRICE;

/// @notice Restricts the calls to the governance address
modifier onlyGovernance() {
// TODO: where is governance address defined?
_;
}

/// @notice Restricts the calls to the dispute module
modifier onlyDisputeModule() {
if (msg.sender != DISPUTE_MODULE) revert Errors.ArbitrationPolicySP__NotDisputeModule();
_;
}

/// @notice Constructor
/// @param _disputeModule Address of the dispute module contract
/// @param _paymentToken Address of the payment token
/// @param _arbitrationPrice Arbitration price
constructor(address _disputeModule, address _paymentToken, uint256 _arbitrationPrice) {
if (_disputeModule == address(0)) revert Errors.ArbitrationPolicySP__ZeroDisputeModule();
if (_paymentToken == address(0)) revert Errors.ArbitrationPolicySP__ZeroPaymentToken();

DISPUTE_MODULE = _disputeModule;
PAYMENT_TOKEN = _paymentToken;
ARBITRATION_PRICE = _arbitrationPrice;
}

/// @notice Executes custom logic on raise dispute
/// @param _caller Address of the caller
function onRaiseDispute(address _caller, bytes calldata) external onlyDisputeModule {
// TODO: we can add permit if the token supports it
IERC20(PAYMENT_TOKEN).safeTransferFrom(_caller, address(this), ARBITRATION_PRICE);
}

/// @notice Executes custom logic on dispute judgement
/// @param _disputeId The dispute id
/// @param _decision The decision of the dispute
function onDisputeJudgement(uint256 _disputeId, bool _decision, bytes calldata) external onlyDisputeModule {
if (_decision) {
(, address disputeInitiator,,,) = IDisputeModule(DISPUTE_MODULE).disputes(_disputeId);
IERC20(PAYMENT_TOKEN).safeTransfer(disputeInitiator, ARBITRATION_PRICE);
}
}

/// @notice Executes custom logic on dispute cancel
function onDisputeCancel(address, uint256, bytes calldata) external onlyDisputeModule {}

/// @notice Allows governance address to withdraw
/// @param _amount The amount to withdraw
function withdraw(uint256 _amount) external onlyGovernance {
// TODO: where is governance address defined?
/* IERC20(PAYMENT_TOKEN).safeTransfer(governance, _amount); */
}
}
80 changes: 80 additions & 0 deletions contracts/modules/royalty-module/RoyaltyModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.23;

import {IRoyaltyModule} from "../../../interfaces/modules/royalty-module/IRoyaltyModule.sol";
import {IRoyaltyPolicy} from "../../../interfaces/modules/royalty-module/policies/IRoyaltyPolicy.sol";

import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

import {Errors} from "../../lib/Errors.sol";

/// @title Story Protocol Royalty Module
/// @notice The Story Protocol royalty module allows to set royalty policies an ipId
/// and pay royalties as a derivative ip.
contract RoyaltyModule is IRoyaltyModule, ReentrancyGuard {
/// @notice Indicates if a royalty policy is whitelisted
mapping(address royaltyPolicy => bool allowed) public isWhitelistedRoyaltyPolicy;

/// @notice Indicates the royalty policy for a given ipId
mapping(address ipId => address royaltyPolicy) public royaltyPolicies;

/// @notice Restricts the calls to the governance address
modifier onlyGovernance() {
// TODO: where is governance address defined?
_;
}

/// @notice Restricts the calls to the license module
modifier onlyLicenseModule() {
// TODO: where is license module address defined?
_;
}

/// @notice Restricts the calls to a IPAccount
modifier onlyIPAccount() {
// TODO: where to find if an address is a valid IPAccount or an approved operator?
_;
}

/// @notice Whitelist a royalty policy
/// @param _royaltyPolicy The address of the royalty policy
/// @param _allowed Indicates if the royalty policy is whitelisted or not
function whitelistRoyaltyPolicy(address _royaltyPolicy, bool _allowed) external onlyGovernance {
if (_royaltyPolicy == address(0)) revert Errors.RoyaltyModule__ZeroRoyaltyPolicy();

isWhitelistedRoyaltyPolicy[_royaltyPolicy] = _allowed;

// TODO: emit event
}

/// @notice Sets the royalty policy for an ipId
/// @param _ipId The ipId
/// @param _royaltyPolicy The address of the royalty policy
/// @param _data The data to initialize the policy
function setRoyaltyPolicy(address _ipId, address _royaltyPolicy, bytes calldata _data)
external
onlyLicenseModule
nonReentrant
{
// TODO: make call to ensure ipId exists/has been registered
if (!isWhitelistedRoyaltyPolicy[_royaltyPolicy]) revert Errors.RoyaltyModule__NotWhitelistedRoyaltyPolicy();
if (royaltyPolicies[_ipId] != address(0)) revert Errors.RoyaltyModule__AlreadySetRoyaltyPolicy();
// TODO: check if royalty policy is compatible with parents royalty policy

royaltyPolicies[_ipId] = _royaltyPolicy;

IRoyaltyPolicy(_royaltyPolicy).initPolicy(_ipId, _data);

// TODO: emit event
}

/// @notice Allows an IPAccount to pay royalties
/// @param _ipId The ipId
/// @param _token The token to pay the royalties in
/// @param _amount The amount to pay
function payRoyalty(address _ipId, address _token, uint256 _amount) external onlyIPAccount nonReentrant {
IRoyaltyPolicy(royaltyPolicies[_ipId]).onRoyaltyPayment(msg.sender, _ipId, _token, _amount);

// TODO: emit event
}
}
Loading