diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7166555f9..c4784747c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 649556bc9..74d0dcda4 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -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 // //////////////////////////////////////////////////////////////////////////// diff --git a/contracts/modules/dispute-module/DisputeModule.sol b/contracts/modules/dispute-module/DisputeModule.sol new file mode 100644 index 000000000..5747e4f6b --- /dev/null +++ b/contracts/modules/dispute-module/DisputeModule.sol @@ -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; + + /// @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 + isWhitelistedArbitrationRelayer; + + /// @notice Restricts the calls to the governance address + modifier onlyGovernance() { + // 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 + + 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 { + 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); + + // 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 + } +} diff --git a/contracts/modules/dispute-module/policies/ArbitrationPolicySP.sol b/contracts/modules/dispute-module/policies/ArbitrationPolicySP.sol new file mode 100644 index 000000000..5cfc3be3f --- /dev/null +++ b/contracts/modules/dispute-module/policies/ArbitrationPolicySP.sol @@ -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); */ + } +} diff --git a/contracts/modules/royalty-module/RoyaltyModule.sol b/contracts/modules/royalty-module/RoyaltyModule.sol new file mode 100644 index 000000000..d866ab4c9 --- /dev/null +++ b/contracts/modules/royalty-module/RoyaltyModule.sol @@ -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 + } +} diff --git a/contracts/modules/royalty-module/policies/RoyaltyPolicyLS.sol b/contracts/modules/royalty-module/policies/RoyaltyPolicyLS.sol new file mode 100644 index 000000000..36ac72672 --- /dev/null +++ b/contracts/modules/royalty-module/policies/RoyaltyPolicyLS.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import {ILiquidSplitFactory} from "../../../../interfaces/modules/royalty-module/policies/ILiquidSplitFactory.sol"; +import {IRoyaltyPolicy} from "../../../../interfaces/modules/royalty-module/policies/IRoyaltyPolicy.sol"; +import {ILiquidSplitClone} from "../../../../interfaces/modules/royalty-module/policies/ILiquidSplitClone.sol"; +import {ILiquidSplitMain} from "../../../../interfaces/modules/royalty-module/policies/ILiquidSplitMain.sol"; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {Errors} from "../../../lib/Errors.sol"; + +/// @title Liquid Split Royalty Policy +/// @notice The LiquidSplit royalty policy splits royalties in accordance with +/// the percentage of royalty NFTs owned by each account. +contract RoyaltyPolicyLS is IRoyaltyPolicy { + using SafeERC20 for IERC20; + + /// @notice RoyaltyModule address + address public immutable ROYALTY_MODULE; + + /// @notice LiquidSplitFactory address + address public immutable LIQUID_SPLIT_FACTORY; + + /// @notice LiquidSplitMain address + address public immutable LIQUID_SPLIT_MAIN; + + /// @notice Indicates the address of the LiquidSplitClone contract for a given ipId + mapping(address ipId => address splitClone) public splitClones; + + /// @notice Restricts the calls to the royalty module + modifier onlyRoyaltyModule() { + if (msg.sender != ROYALTY_MODULE) revert Errors.RoyaltyPolicyLS__NotRoyaltyModule(); + _; + } + + /// @notice Constructor + /// @param _royaltyModule Address of the RoyaltyModule contract + /// @param _liquidSplitFactory Address of the LiquidSplitFactory contract + /// @param _liquidSplitMain Address of the LiquidSplitMain contract + constructor(address _royaltyModule, address _liquidSplitFactory, address _liquidSplitMain) { + if (_royaltyModule == address(0)) revert Errors.RoyaltyPolicyLS__ZeroRoyaltyModule(); + if (_liquidSplitFactory == address(0)) revert Errors.RoyaltyPolicyLS__ZeroLiquidSplitFactory(); + if (_liquidSplitMain == address(0)) revert Errors.RoyaltyPolicyLS__ZeroLiquidSplitMain(); + + ROYALTY_MODULE = _royaltyModule; + LIQUID_SPLIT_FACTORY = _liquidSplitFactory; + LIQUID_SPLIT_MAIN = _liquidSplitMain; + } + + /// @notice Initializes the royalty policy + /// @param _ipId The ipId + /// @param _data The data to initialize the policy + function initPolicy(address _ipId, bytes calldata _data) external onlyRoyaltyModule { + (address[] memory accounts, uint32[] memory initAllocations, uint32 distributorFee, address splitOwner) = + abi.decode(_data, (address[], uint32[], uint32, address)); + + // TODO: input validation: accounts & initAllocations - can we make up to 1000 parents with tx going through - if not alternative may be to create new contract to claim RNFTs + // TODO: input validation: distributorFee + // TODO: input validation: splitOwner + + address splitClone = ILiquidSplitFactory(LIQUID_SPLIT_FACTORY).createLiquidSplitClone( + accounts, initAllocations, distributorFee, splitOwner + ); + + splitClones[_ipId] = splitClone; + } + + /// @notice Distributes funds to the accounts in the LiquidSplitClone contract + /// @param _ipId The ipId + /// @param _token The token to distribute + /// @param _accounts The accounts to distribute to + /// @param _distributorAddress The distributor address + function distributeFunds(address _ipId, address _token, address[] calldata _accounts, address _distributorAddress) + external + { + ILiquidSplitClone(splitClones[_ipId]).distributeFunds(_token, _accounts, _distributorAddress); + } + + /// @notice Claims the available royalties for a given account + /// @param _account The account to claim for + /// @param _withdrawETH The amount of ETH to withdraw + /// @param _tokens The tokens to withdraw + function claimRoyalties(address _account, uint256 _withdrawETH, ERC20[] calldata _tokens) external { + ILiquidSplitMain(LIQUID_SPLIT_MAIN).withdraw(_account, _withdrawETH, _tokens); + } + + /// @notice Allows to pay a royalty + /// @param _caller The caller + /// @param _ipId The ipId + /// @param _token The token to pay + /// @param _amount The amount to pay + function onRoyaltyPayment(address _caller, address _ipId, address _token, uint256 _amount) + external + onlyRoyaltyModule + { + address destination = splitClones[_ipId]; + IERC20(_token).safeTransferFrom(_caller, destination, _amount); + } +} diff --git a/contracts/utils/ShortStringOps.sol b/contracts/utils/ShortStringOps.sol new file mode 100644 index 000000000..8dbf6366a --- /dev/null +++ b/contracts/utils/ShortStringOps.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +// See https://github.com/storyprotocol/protocol-contracts/blob/main/StoryProtocol-AlphaTestingAgreement-17942166.3.pdf +pragma solidity ^0.8.19; + +import { ShortString, ShortStrings } from "@openzeppelin/contracts/utils/ShortStrings.sol"; + +/// @notice Library for working with Openzeppelin's ShortString data types. +library ShortStringEquals { + using ShortStrings for *; + + /// @dev Compares whether two ShortStrings are equal. + function equal(ShortString a, ShortString b) internal pure returns (bool) { + return ShortString.unwrap(a) == ShortString.unwrap(b); + } + + /// @dev Checks whether a ShortString and a regular string are equal. + function equal(ShortString a, string memory b) internal pure returns (bool) { + return equal(a, b.toShortString()); + } + + /// @dev Checks whether a regular string and a ShortString are equal. + function equal(string memory a, ShortString b) internal pure returns (bool) { + return equal(a.toShortString(), b); + } + + /// @dev Checks whether a bytes32 object and ShortString are equal. + function equal(bytes32 a, ShortString b) internal pure returns (bool) { + return a == ShortString.unwrap(b); + } + + /// @dev Checks whether a string and bytes32 object are equal. + function equal(string memory a, bytes32 b) internal pure returns (bool) { + return equal(a, ShortString.wrap(b)); + } + + /// @dev Checks whether a bytes32 object and string are equal. + function equal(bytes32 a, string memory b) internal pure returns (bool) { + return equal(ShortString.wrap(a), b); + } + + function stringToBytes32(string memory s) internal pure returns (bytes32) { + return ShortString.unwrap(s.toShortString()); + } +} + +library EnumerableShortStringSet { + +} \ No newline at end of file diff --git a/interfaces/modules/dispute-module/IDisputeModule.sol b/interfaces/modules/dispute-module/IDisputeModule.sol new file mode 100644 index 000000000..3bdbeb6d2 --- /dev/null +++ b/interfaces/modules/dispute-module/IDisputeModule.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +/// @title Dispute Module Interface +interface IDisputeModule { + /// @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; + + /// @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; + + /// @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; + + /// @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 returns (uint256 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; + + /// @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; + + /// @notice Resolves a dispute after it has been judged + /// @param disputeId The dispute id + function resolveDispute(uint256 disputeId) external; + + /// @notice Gets the dispute struct characteristics + function disputes(uint256 disputeId) external view returns ( + address ipId, + address disputeInitiator, + address arbitrationPolicy, + bytes32 linkToDisputeSummary, + bytes32 tag + ); +} \ No newline at end of file diff --git a/interfaces/modules/dispute-module/policies/IArbitrationPolicy.sol b/interfaces/modules/dispute-module/policies/IArbitrationPolicy.sol new file mode 100644 index 000000000..b6c6ed05f --- /dev/null +++ b/interfaces/modules/dispute-module/policies/IArbitrationPolicy.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +/// @title ArbitrationPolicy interface +interface IArbitrationPolicy { + /// @notice Executes custom logic on raise dispute + /// @param caller Address of the caller + function onRaiseDispute(address caller, bytes calldata data) external; + + /// @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 data) external; + + /// @notice Executes custom logic on dispute cancel + function onDisputeCancel(address caller, uint256 disputeId, bytes calldata data) external; +} diff --git a/interfaces/modules/royalty-module/IRoyaltyModule.sol b/interfaces/modules/royalty-module/IRoyaltyModule.sol new file mode 100644 index 000000000..a75f8bb23 --- /dev/null +++ b/interfaces/modules/royalty-module/IRoyaltyModule.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +/// @title RoyaltyModule interface +interface IRoyaltyModule { + /// @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; + + /// @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; + + /// @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; + + /// @notice Gets the royalty policy for a given ipId + function royaltyPolicies(address ipId) external view returns (address royaltyPolicy); +} \ No newline at end of file diff --git a/interfaces/modules/royalty-module/policies/ILiquidSplitClone.sol b/interfaces/modules/royalty-module/policies/ILiquidSplitClone.sol new file mode 100644 index 000000000..f8ece07cb --- /dev/null +++ b/interfaces/modules/royalty-module/policies/ILiquidSplitClone.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +/// @title LiquidSplitClone interface +interface ILiquidSplitClone { + /// @notice Distributes funds to the accounts in the LiquidSplitClone contract + /// @param token The token to distribute + /// @param accounts The accounts to distribute to + /// @param distributorAddress The distributor address + function distributeFunds(address token, address[] calldata accounts, address distributorAddress) external; +} \ No newline at end of file diff --git a/interfaces/modules/royalty-module/policies/ILiquidSplitFactory.sol b/interfaces/modules/royalty-module/policies/ILiquidSplitFactory.sol new file mode 100644 index 000000000..da9a630b5 --- /dev/null +++ b/interfaces/modules/royalty-module/policies/ILiquidSplitFactory.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +/// @title LiquidSplitFactory interface +interface ILiquidSplitFactory { + /// @notice Creates a new LiquidSplitClone contract + /// @param accounts The accounts to initialize the LiquidSplitClone contract with + /// @param initAllocations The initial allocations + /// @param _distributorFee The distributor fee + /// @param owner The owner of the LiquidSplitClone contract + function createLiquidSplitClone( + address[] calldata accounts, + uint32[] calldata initAllocations, + uint32 _distributorFee, + address owner + ) external returns (address); +} \ No newline at end of file diff --git a/interfaces/modules/royalty-module/policies/ILiquidSplitMain.sol b/interfaces/modules/royalty-module/policies/ILiquidSplitMain.sol new file mode 100644 index 000000000..ba1380935 --- /dev/null +++ b/interfaces/modules/royalty-module/policies/ILiquidSplitMain.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/// @title LiquidSplitMain interface +interface ILiquidSplitMain { + /// @notice Allows an account to withdraw their accrued and distributed pending amount + /// @param account The account to withdraw from + /// @param withdrawETH The amount of ETH to withdraw + /// @param tokens The tokens to withdraw + function withdraw( + address account, + uint256 withdrawETH, + ERC20[] calldata tokens + ) external; +} \ No newline at end of file diff --git a/interfaces/modules/royalty-module/policies/IRoyaltyPolicy.sol b/interfaces/modules/royalty-module/policies/IRoyaltyPolicy.sol new file mode 100644 index 000000000..ad97f6b61 --- /dev/null +++ b/interfaces/modules/royalty-module/policies/IRoyaltyPolicy.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +/// @title RoyaltyPolicy interface +interface IRoyaltyPolicy { + /// @notice Initializes the royalty policy + /// @param ipId The ipId + /// @param data The data to initialize the policy + function initPolicy(address ipId, bytes calldata data) external; + + /// @notice Allows to pay a royalty + /// @param caller The caller + /// @param ipId The ipId + /// @param token The token to pay + /// @param amount The amount to pay + function onRoyaltyPayment(address caller, address ipId, address token, uint256 amount) external; +} \ No newline at end of file diff --git a/test/foundry/DisputeModule.t.sol b/test/foundry/DisputeModule.t.sol new file mode 100644 index 000000000..ba555567f --- /dev/null +++ b/test/foundry/DisputeModule.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import {console2} from "forge-std/console2.sol"; +import {TestHelper} from "./../utils/TestHelper.sol"; + +import {ShortStringEquals} from "./../../contracts/utils/ShortStringOps.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract TestDisputeModule is TestHelper { + function setUp() public override { + super.setUp(); + + // fund USDC + vm.startPrank(USDC_RICH); + IERC20(USDC).transfer(ipAccount1, 1000 * 10 ** 6); + vm.stopPrank(); + + // whitelist dispute tag + disputeModule.whitelistDisputeTags("plagiarism", true); + + // whitelist arbitration policy + disputeModule.whitelistArbitrationPolicy(address(arbitrationPolicySP), true); + + // whitelist arbitration relayer + disputeModule.whitelistArbitrationRelayer(address(arbitrationPolicySP), arbitrationRelayer, true); + } + + function test_DisputeModule_whitelistDisputeTags() public { + assertEq(disputeModule.isWhitelistedDisputeTag("plagiarism"), true); + } + + function test_DisputeModule_whitelistArbitrationPolicy() public { + assertEq(disputeModule.isWhitelistedArbitrationPolicy(address(arbitrationPolicySP)), true); + } + + function test_DisputeModule_whitelistArbitrationRelayer() public { + assertEq( + disputeModule.isWhitelistedArbitrationRelayer(address(arbitrationPolicySP), address(arbitrationRelayer)), + true + ); + } + + function test_DisputeModule_raiseDispute() public { + vm.startPrank(ipAccount1); + + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + + uint256 disputeIdBefore = disputeModule.disputeId(); + uint256 ipAccount1USDCBalanceBefore = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceBefore = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + disputeModule.raiseDispute(address(1), address(arbitrationPolicySP), string("urlExample"), "plagiarism", ""); + + uint256 disputeIdAfter = disputeModule.disputeId(); + uint256 ipAccount1USDCBalanceAfter = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceAfter = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + (address ip_id, address disputeInitiator, address arbitrationPolicy, bytes32 linkToDisputeSummary, bytes32 tag) + = disputeModule.disputes(disputeIdAfter); + + assertEq(disputeIdAfter - disputeIdBefore, 1); + assertEq(ipAccount1USDCBalanceBefore - ipAccount1USDCBalanceAfter, ARBITRATION_PRICE); + assertEq(arbitrationPolicySPUSDCBalanceAfter - arbitrationPolicySPUSDCBalanceBefore, ARBITRATION_PRICE); + assertEq(ip_id, address(1)); + assertEq(disputeInitiator, ipAccount1); + assertEq(arbitrationPolicy, address(arbitrationPolicySP)); + assertEq(linkToDisputeSummary, ShortStringEquals.stringToBytes32("urlExample")); + assertEq(tag, bytes32("plagiarism")); + } + + function test_DisputeModule_setDisputeJudgement() public { + // raise dispute + vm.startPrank(ipAccount1); + IERC20(USDC).approve(address(arbitrationPolicySP), ARBITRATION_PRICE); + disputeModule.raiseDispute(address(1), address(arbitrationPolicySP), string("urlExample"), "plagiarism", ""); + vm.stopPrank(); + + // set dispute judgement + vm.startPrank(arbitrationRelayer); + uint256 ipAccount1USDCBalanceBefore = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceBefore = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + + disputeModule.setDisputeJudgement(1, true, ""); + + uint256 ipAccount1USDCBalanceAfter = IERC20(USDC).balanceOf(ipAccount1); + uint256 arbitrationPolicySPUSDCBalanceAfter = IERC20(USDC).balanceOf(address(arbitrationPolicySP)); + vm.stopPrank(); + + assertEq(ipAccount1USDCBalanceAfter - ipAccount1USDCBalanceBefore, ARBITRATION_PRICE); + assertEq(arbitrationPolicySPUSDCBalanceBefore - arbitrationPolicySPUSDCBalanceAfter, ARBITRATION_PRICE); + } +} diff --git a/test/foundry/RoyaltyModule.t.sol b/test/foundry/RoyaltyModule.t.sol new file mode 100644 index 000000000..dcd9e28da --- /dev/null +++ b/test/foundry/RoyaltyModule.t.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import {console2} from "forge-std/console2.sol"; +import {TestHelper} from "./../utils/TestHelper.sol"; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract TestRoyaltyModule is TestHelper { + function setUp() public override { + super.setUp(); + + // fund USDC + vm.startPrank(USDC_RICH); + IERC20(USDC).transfer(ipAccount4, 1000 * 10 ** 6); + vm.stopPrank(); + + // whitelist royalty policy + royaltyModule.whitelistRoyaltyPolicy(address(royaltyPolicyLS), true); + } + + function test_RoyaltyModule_whitelistRoyaltyPolicy() public { + assertEq(royaltyModule.isWhitelistedRoyaltyPolicy(address(royaltyPolicyLS)), true); + } + + function test_RoyaltyModule_setRoyalty() public { + vm.startPrank(ipAccount3); + + address[] memory accounts = new address[](2); + accounts[0] = ipAccount1; + accounts[1] = ipAccount2; + + uint32[] memory initAllocations = new uint32[](2); + initAllocations[0] = 100; + initAllocations[1] = 900; + + bytes memory data = abi.encode(accounts, initAllocations, uint32(0), address(0)); + + royaltyModule.setRoyaltyPolicy(ipAccount3, address(royaltyPolicyLS), data); + + assertEq(royaltyModule.royaltyPolicies(ipAccount3), address(royaltyPolicyLS)); + // TODO: assertNotEq(royaltyModule.royaltyPolicies(ipAccount3), address(0)); // assertNotEq was deprecated? + } + + function test_RoyaltyModule_payRoyalty() public { + vm.startPrank(ipAccount3); + + address[] memory accounts = new address[](2); + accounts[0] = ipAccount1; + accounts[1] = ipAccount2; + + uint32[] memory initAllocations = new uint32[](2); + initAllocations[0] = 100; + initAllocations[1] = 900; + + bytes memory data = abi.encode(accounts, initAllocations, uint32(0), address(0)); + + royaltyModule.setRoyaltyPolicy(ipAccount3, address(royaltyPolicyLS), data); + vm.stopPrank(); + + vm.startPrank(ipAccount4); + uint256 royaltyAmount = 100 * 10 ** 6; + IERC20(USDC).approve(address(royaltyPolicyLS), royaltyAmount); + + uint256 ipAccount4USDCBalanceBefore = IERC20(USDC).balanceOf(ipAccount4); + uint256 splitCloneUSDCBalanceBefore = IERC20(USDC).balanceOf(royaltyPolicyLS.splitClones(ipAccount3)); + + royaltyModule.payRoyalty(ipAccount3, USDC, royaltyAmount); + + uint256 ipAccount4USDCBalanceAfter = IERC20(USDC).balanceOf(ipAccount4); + uint256 splitCloneUSDCBalanceAfter = IERC20(USDC).balanceOf(royaltyPolicyLS.splitClones(ipAccount3)); + + assertEq(ipAccount4USDCBalanceBefore - ipAccount4USDCBalanceAfter, royaltyAmount); + assertEq(splitCloneUSDCBalanceAfter - splitCloneUSDCBalanceBefore, royaltyAmount); + } + + // TODO: move to royalty policy test file when created + function test_RoyaltyModule_distributeFunds() public { + vm.startPrank(ipAccount3); + + address[] memory accounts = new address[](2); + accounts[0] = ipAccount1; + accounts[1] = ipAccount2; + + uint32[] memory initAllocations = new uint32[](2); + initAllocations[0] = 100; + initAllocations[1] = 900; + + bytes memory data = abi.encode(accounts, initAllocations, uint32(0), address(0)); + + royaltyModule.setRoyaltyPolicy(ipAccount3, address(royaltyPolicyLS), data); + vm.stopPrank(); + + vm.startPrank(ipAccount4); + uint256 royaltyAmount = 100 * 10 ** 6; + IERC20(USDC).approve(address(royaltyPolicyLS), royaltyAmount); + + royaltyModule.payRoyalty(ipAccount3, USDC, royaltyAmount); + vm.stopPrank(); + + vm.startPrank(ipAccount2); + + uint256 splitCloneUSDCBalanceBefore = IERC20(USDC).balanceOf(royaltyPolicyLS.splitClones(ipAccount3)); + uint256 splitMainUSDCBalanceBefore = IERC20(USDC).balanceOf(royaltyPolicyLS.LIQUID_SPLIT_MAIN()); + + royaltyPolicyLS.distributeFunds(ipAccount3, USDC, accounts, address(2)); + + uint256 splitCloneUSDCBalanceAfter = IERC20(USDC).balanceOf(royaltyPolicyLS.splitClones(ipAccount3)); + uint256 splitMainUSDCBalanceAfter = IERC20(USDC).balanceOf(royaltyPolicyLS.LIQUID_SPLIT_MAIN()); + + assertApproxEqRel(splitCloneUSDCBalanceBefore - splitCloneUSDCBalanceAfter, royaltyAmount, 0.0001e18); + assertApproxEqRel(splitMainUSDCBalanceAfter - splitMainUSDCBalanceBefore, royaltyAmount, 0.0001e18); + } + + // TODO: move to royalty policy test file when created + function test_RoyaltyModule_claimRoyalties() public { + vm.startPrank(ipAccount3); + + address[] memory accounts = new address[](2); + accounts[0] = ipAccount1; + accounts[1] = ipAccount2; + + uint32[] memory initAllocations = new uint32[](2); + initAllocations[0] = 100; + initAllocations[1] = 900; + + bytes memory data = abi.encode(accounts, initAllocations, uint32(0), address(0)); + + royaltyModule.setRoyaltyPolicy(ipAccount3, address(royaltyPolicyLS), data); + vm.stopPrank(); + + vm.startPrank(ipAccount4); + uint256 royaltyAmount = 100 * 10 ** 6; + IERC20(USDC).approve(address(royaltyPolicyLS), royaltyAmount); + + royaltyModule.payRoyalty(ipAccount3, USDC, royaltyAmount); + vm.stopPrank(); + + vm.startPrank(ipAccount2); + royaltyPolicyLS.distributeFunds(ipAccount3, USDC, accounts, address(2)); + + ERC20[] memory tokens = new ERC20[](1); + tokens[0] = ERC20(USDC); + + uint256 ipAccount2USDCBalanceBefore = IERC20(USDC).balanceOf(ipAccount2); + uint256 splitMainUSDCBalanceBefore = IERC20(USDC).balanceOf(royaltyPolicyLS.LIQUID_SPLIT_MAIN()); + + royaltyPolicyLS.claimRoyalties(ipAccount2, 0, tokens); + + uint256 ipAccount2USDCBalanceAfter = IERC20(USDC).balanceOf(ipAccount2); + uint256 splitMainUSDCBalanceAfter = IERC20(USDC).balanceOf(royaltyPolicyLS.LIQUID_SPLIT_MAIN()); + + assertApproxEqRel(ipAccount2USDCBalanceAfter - ipAccount2USDCBalanceBefore, 90 * 10 ** 6, 0.0001e18); + assertApproxEqRel(splitMainUSDCBalanceBefore - splitMainUSDCBalanceAfter, 90 * 10 ** 6, 0.0001e18); + } +} diff --git a/test/utils/DeployHelper.sol b/test/utils/DeployHelper.sol new file mode 100644 index 000000000..8f7d27760 --- /dev/null +++ b/test/utils/DeployHelper.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; + +import {DisputeModule} from "../../contracts/modules/dispute-module/DisputeModule.sol"; +import {ArbitrationPolicySP} from "../../contracts/modules/dispute-module/policies/ArbitrationPolicySP.sol"; +import {RoyaltyModule} from "../../contracts/modules/royalty-module/RoyaltyModule.sol"; +import {RoyaltyPolicyLS} from "../../contracts/modules/royalty-module/policies/RoyaltyPolicyLS.sol"; + +contract DeployHelper is Test { + DisputeModule public disputeModule; + ArbitrationPolicySP public arbitrationPolicySP; + RoyaltyModule public royaltyModule; + RoyaltyPolicyLS public royaltyPolicyLS; + + uint256 public constant ARBITRATION_PRICE = 1000 * 10 ** 6; // 1000 USDC + + // USDC + string public constant USDC_NAME = "USD Coin"; + string public constant USDC_SYMBOL = "USDC"; + uint8 public constant USDC_DECIMALS = 6; + address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + address public constant USDC_RICH = 0xcEe284F754E854890e311e3280b767F80797180d; + + // Liquid Split (ETH Mainnet) + address public constant LIQUID_SPLIT_FACTORY = 0xdEcd8B99b7F763e16141450DAa5EA414B7994831; + address public constant LIQUID_SPLIT_MAIN = 0x2ed6c4B5dA6378c7897AC67Ba9e43102Feb694EE; + + function deploy() public { + disputeModule = new DisputeModule(); + arbitrationPolicySP = new ArbitrationPolicySP(address(disputeModule), USDC, ARBITRATION_PRICE); + royaltyModule = new RoyaltyModule(); + royaltyPolicyLS = new RoyaltyPolicyLS(address(royaltyModule), LIQUID_SPLIT_FACTORY, LIQUID_SPLIT_MAIN); + + vm.label(address(disputeModule), "disputeModule"); + vm.label(address(arbitrationPolicySP), "arbitrationPolicySP"); + vm.label(address(royaltyModule), "royaltyModule"); + vm.label(address(royaltyPolicyLS), "royaltyPolicyLS"); + } +} diff --git a/test/utils/TestHelper.sol b/test/utils/TestHelper.sol new file mode 100644 index 000000000..ab338f8de --- /dev/null +++ b/test/utils/TestHelper.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.23; + +import {console2} from "forge-std/console2.sol"; +import {Test} from "forge-std/Test.sol"; +import {DeployHelper} from "./DeployHelper.sol"; + +contract TestHelper is Test, DeployHelper { + uint256 internal constant accountA = 1; + uint256 internal constant accountB = 2; + uint256 internal constant accountC = 3; + uint256 internal constant accountD = 4; + uint256 internal constant accountE = 5; + uint256 internal constant accountF = 6; + uint256 internal constant accountG = 7; + + address internal deployer; + address internal governance; + address internal arbitrationRelayer; + address internal ipAccount1; + address internal ipAccount2; + address internal ipAccount3; + address internal ipAccount4; + + function setUp() public virtual { + deployer = vm.addr(accountA); + governance = vm.addr(accountB); + arbitrationRelayer = vm.addr(accountC); + ipAccount1 = vm.addr(accountD); + ipAccount2 = vm.addr(accountE); + ipAccount3 = vm.addr(accountF); + ipAccount4 = vm.addr(accountG); + + deploy(); + + vm.label(deployer, "deployer"); + vm.label(governance, "governance"); + vm.label(arbitrationRelayer, "arbitrationRelayer"); + vm.label(ipAccount1, "ipAccount1"); + vm.label(ipAccount2, "ipAccount2"); + vm.label(ipAccount3, "ipAccount3"); + vm.label(ipAccount4, "ipAccount4"); + } +}