From 3ef2a99a99192587d24b4805d5848e3a799dcf5e Mon Sep 17 00:00:00 2001 From: Spablob <99089658+Spablob@users.noreply.github.com> Date: Sat, 19 Oct 2024 07:19:47 +0100 Subject: [PATCH] Add new Dispute Arbitration Policy (#263) * add draft new arbitration policy interfaces * add new arbitration policy errors * add new arbitration policy skeleton * add base integration unit tests * update deploy helper * fix DisputeResolved event * fix DisputeResolve unit tests * update Errors.sol * add new arbitration policy updates * interface update * solhint fix * update Errors.sol * format fixes * add new disputeAssertion function - contracts and unit tests * DeployHelper.sol fix * add dispute timestamp to DisputeModule.sol * add ipOwner dispute assertion priority time window * interface fixes * error naming fix * deploy helper naming fix * add interface function * ArbitrationPolicyUMA contract adjustments and unit tests * comment fix * enhance error message * format fix --- .../modules/dispute/IDisputeModule.sol | 13 +- .../dispute/policies/IArbitrationPolicy.sol | 12 +- .../policies/UMA/IArbitrationPolicyUMA.sol | 82 ++ .../modules/dispute/policies/UMA/IOOV3.sol | 52 ++ .../dispute/policies/UMA/IOOV3Callbacks.sol | 9 + contracts/lib/Errors.sol | 62 ++ contracts/modules/dispute/DisputeModule.sol | 23 +- .../policies/UMA/ArbitrationPolicyUMA.sol | 287 +++++++ script/foundry/utils/DeployHelper.sol | 46 +- test/foundry/invariants/DisputeModule.t.sol | 1 + test/foundry/mocks/IMockAncillary.sol | 9 + .../mocks/dispute/MockArbitrationPolicy.sol | 2 +- .../mocks/dispute/MockIpAssetRegistry.sol | 8 + .../modules/dispute/DisputeModule.t.sol | 31 +- .../policies/UMA/ArbitrationPolicyUMA.t.sol | 705 ++++++++++++++++++ 15 files changed, 1314 insertions(+), 28 deletions(-) create mode 100644 contracts/interfaces/modules/dispute/policies/UMA/IArbitrationPolicyUMA.sol create mode 100644 contracts/interfaces/modules/dispute/policies/UMA/IOOV3.sol create mode 100644 contracts/interfaces/modules/dispute/policies/UMA/IOOV3Callbacks.sol create mode 100644 contracts/modules/dispute/policies/UMA/ArbitrationPolicyUMA.sol create mode 100644 test/foundry/mocks/IMockAncillary.sol create mode 100644 test/foundry/mocks/dispute/MockIpAssetRegistry.sol create mode 100644 test/foundry/modules/dispute/policies/UMA/ArbitrationPolicyUMA.t.sol diff --git a/contracts/interfaces/modules/dispute/IDisputeModule.sol b/contracts/interfaces/modules/dispute/IDisputeModule.sol index 3310c06c..9b089029 100644 --- a/contracts/interfaces/modules/dispute/IDisputeModule.sol +++ b/contracts/interfaces/modules/dispute/IDisputeModule.sol @@ -6,6 +6,7 @@ interface IDisputeModule { /// @notice Dispute struct /// @param targetIpId The ipId that is the target of the dispute /// @param disputeInitiator The address of the dispute initiator + /// @param disputeTimestamp The timestamp of the dispute /// @param arbitrationPolicy The address of the arbitration policy /// @param disputeEvidenceHash The hash pointing to the dispute evidence /// @param targetTag The target tag of the dispute @@ -14,6 +15,7 @@ interface IDisputeModule { struct Dispute { address targetIpId; address disputeInitiator; + uint256 disputeTimestamp; address arbitrationPolicy; bytes32 disputeEvidenceHash; bytes32 targetTag; @@ -55,6 +57,7 @@ interface IDisputeModule { /// @param disputeId The dispute id /// @param targetIpId The ipId that is the target of the dispute /// @param disputeInitiator The address of the dispute initiator + /// @param disputeTimestamp The timestamp of the dispute /// @param arbitrationPolicy The address of the arbitration policy /// @param disputeEvidenceHash The hash pointing to the dispute evidence /// @param targetTag The target tag of the dispute @@ -63,6 +66,7 @@ interface IDisputeModule { uint256 disputeId, address targetIpId, address disputeInitiator, + uint256 disputeTimestamp, address arbitrationPolicy, bytes32 disputeEvidenceHash, bytes32 targetTag, @@ -85,16 +89,19 @@ interface IDisputeModule { /// @param derivativeIpId The derivative ipId which was tagged /// @param parentDisputeId The parent dispute id in which infringement was found /// @param tag The tag of the dispute applied to the derivative + /// @param disputeTimestamp The timestamp of the dispute event DerivativeTaggedOnParentInfringement( address parentIpId, address derivativeIpId, uint256 parentDisputeId, - bytes32 tag + bytes32 tag, + uint256 disputeTimestamp ); /// @notice Event emitted when a dispute is resolved /// @param disputeId The dispute id - event DisputeResolved(uint256 disputeId); + /// @param data Custom data adjusted to each policy + event DisputeResolved(uint256 disputeId, bytes data); /// @notice Dispute ID counter function disputeCounter() external view returns (uint256); @@ -106,6 +113,7 @@ interface IDisputeModule { /// @param disputeId The dispute id /// @return targetIpId The ipId that is the target of the dispute /// @return disputeInitiator The address of the dispute initiator + /// @return disputeTimestamp The timestamp of the dispute /// @return arbitrationPolicy The address of the arbitration policy /// @return disputeEvidenceHash The link of the dispute summary /// @return targetTag The target tag of the dispute @@ -119,6 +127,7 @@ interface IDisputeModule { returns ( address targetIpId, address disputeInitiator, + uint256 disputeTimestamp, address arbitrationPolicy, bytes32 disputeEvidenceHash, bytes32 targetTag, diff --git a/contracts/interfaces/modules/dispute/policies/IArbitrationPolicy.sol b/contracts/interfaces/modules/dispute/policies/IArbitrationPolicy.sol index b00c6928..f8b38dcf 100644 --- a/contracts/interfaces/modules/dispute/policies/IArbitrationPolicy.sol +++ b/contracts/interfaces/modules/dispute/policies/IArbitrationPolicy.sol @@ -3,21 +3,21 @@ pragma solidity 0.8.26; /// @title Arbitration Policy Interface interface IArbitrationPolicy { - /// @notice Executes custom logic on raising dispute. - /// @dev Enforced to be only callable by the DisputeModule. + /// @notice Executes custom logic on raising dispute + /// @dev Enforced to be only callable by the DisputeModule /// @param caller Address of the caller /// @param data The arbitrary data used to raise the dispute function onRaiseDispute(address caller, bytes calldata data) external; - /// @notice Executes custom logic on disputing judgement. - /// @dev Enforced to be only callable by the DisputeModule. + /// @notice Executes custom logic on disputing judgement + /// @dev Enforced to be only callable by the DisputeModule /// @param disputeId The dispute id /// @param decision The decision of the dispute /// @param data The arbitrary data used to set the dispute judgement function onDisputeJudgement(uint256 disputeId, bool decision, bytes calldata data) external; - /// @notice Executes custom logic on disputing cancel. - /// @dev Enforced to be only callable by the DisputeModule. + /// @notice Executes custom logic on disputing cancel + /// @dev Enforced to be only callable by the DisputeModule /// @param caller Address of the caller /// @param disputeId The dispute id /// @param data The arbitrary data used to cancel the dispute diff --git a/contracts/interfaces/modules/dispute/policies/UMA/IArbitrationPolicyUMA.sol b/contracts/interfaces/modules/dispute/policies/UMA/IArbitrationPolicyUMA.sol new file mode 100644 index 00000000..fab9c10b --- /dev/null +++ b/contracts/interfaces/modules/dispute/policies/UMA/IArbitrationPolicyUMA.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { IArbitrationPolicy } from "../IArbitrationPolicy.sol"; +import { IOOV3Callbacks } from "./IOOV3Callbacks.sol"; + +/// @title Arbitration Policy UMA Interface +interface IArbitrationPolicyUMA is IArbitrationPolicy, IOOV3Callbacks { + /// @notice Emitted when liveness is set + /// @param minLiveness The minimum liveness value + /// @param maxLiveness The maximum liveness value + /// @param ipOwnerTimePercent The percentage of liveness time the IP owner has priority to respond to a dispute + event LivenessSet(uint64 minLiveness, uint64 maxLiveness, uint32 ipOwnerTimePercent); + + /// @notice Emitted when max bond is set + /// @param token The token address + /// @param maxBond The maximum bond value + event MaxBondSet(address token, uint256 maxBond); + + /// @notice Emitted when a dispute is raised + /// @param disputeId The dispute id + /// @param caller The caller address that raised the dispute + /// @param claim The asserted claim + /// @param liveness The liveness time + /// @param currency The bond currency + /// @param bond The bond size + /// @param identifier The UMA specific identifier + event DisputeRaisedUMA( + uint256 disputeId, + address caller, + bytes claim, + uint64 liveness, + address currency, + uint256 bond, + bytes32 identifier + ); + + /// @notice Emitted when an assertion is disputed + /// @param assertionId The assertion id + /// @param counterEvidenceHash The counter evidence hash + event AssertionDisputed(bytes32 assertionId, bytes32 counterEvidenceHash); + + /// @notice Sets the liveness for UMA disputes + /// @param minLiveness The minimum liveness value + /// @param maxLiveness The maximum liveness value + /// @param ipOwnerTimePercent The percentage of liveness time the IP owner has priority to respond to a dispute + function setLiveness(uint64 minLiveness, uint64 maxLiveness, uint32 ipOwnerTimePercent) external; + + /// @notice Sets the max bond for UMA disputes + /// @param token The token address + /// @param maxBond The maximum bond value + function setMaxBond(address token, uint256 maxBond) external; + + /// @notice Allows the IP that was targeted with a dispute to dispute the assertion while providing counter evidence + /// @param assertionId The identifier of the assertion that was disputed + /// @param counterEvidenceHash The hash of the counter evidence + function disputeAssertion(bytes32 assertionId, bytes32 counterEvidenceHash) external; + + /// @notice Returns the maximum percentage - represents 100% + function maxPercent() external view returns (uint32); + + /// @notice Returns the minimum liveness for UMA disputes + function minLiveness() external view returns (uint64); + + /// @notice Returns the maximum liveness for UMA disputes + function maxLiveness() external view returns (uint64); + + /// @notice Returns the percentage of liveness time the IP owner has priority to respond to a dispute + function ipOwnerTimePercent() external view returns (uint32); + + /// @notice Returns the maximum bond for a given token for UMA disputes + /// @param token The token address + function maxBonds(address token) external view returns (uint256); + + /// @notice Returns the assertion id for a given dispute id + /// @param disputeId The dispute id + function disputeIdToAssertionId(uint256 disputeId) external view returns (bytes32); + + /// @notice Returns the dispute id for a given assertion id + /// @param assertionId The assertion id + function assertionIdToDisputeId(bytes32 assertionId) external view returns (uint256); +} diff --git a/contracts/interfaces/modules/dispute/policies/UMA/IOOV3.sol b/contracts/interfaces/modules/dispute/policies/UMA/IOOV3.sol new file mode 100644 index 00000000..ddcf688f --- /dev/null +++ b/contracts/interfaces/modules/dispute/policies/UMA/IOOV3.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title IOOV3 Interface +interface IOOV3 { + struct EscalationManagerSettings { + bool arbitrateViaEscalationManager; + bool discardOracle; + bool validateDisputers; + address assertingCaller; + address escalationManager; + } + + struct Assertion { + EscalationManagerSettings escalationManagerSettings; + address asserter; + uint64 assertionTime; + bool settled; + IERC20 currency; + uint64 expirationTime; + bool settlementResolution; + bytes32 domainId; + bytes32 identifier; + uint256 bond; + address callbackRecipient; + address disputer; + } + + function assertTruth( + bytes memory claim, + address asserter, + address callbackRecipient, + address escalationManager, + uint64 liveness, + IERC20 currency, + uint256 bond, + bytes32 identifier, + bytes32 domainId + ) external returns (bytes32 assertionId); + + function disputeAssertion(bytes32 assertionId, address disputer) external; + + function settleAssertion(bytes32 assertionId) external; + + function getAssertion(bytes32 assertionId) external view returns (Assertion memory); + + function stampAssertion(bytes32 assertionId) external view returns (bytes memory); + + function burnedBondPercentage() external view returns (uint256); +} diff --git a/contracts/interfaces/modules/dispute/policies/UMA/IOOV3Callbacks.sol b/contracts/interfaces/modules/dispute/policies/UMA/IOOV3Callbacks.sol new file mode 100644 index 00000000..d842987c --- /dev/null +++ b/contracts/interfaces/modules/dispute/policies/UMA/IOOV3Callbacks.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +/// @title IOOV3 Callbacks Interface +interface IOOV3Callbacks { + function assertionResolvedCallback(bytes32 assertionId, bool assertedTruthfully) external; + + function assertionDisputedCallback(bytes32 assertionId) external; +} diff --git a/contracts/lib/Errors.sol b/contracts/lib/Errors.sol index 53d5676c..f78f7d52 100644 --- a/contracts/lib/Errors.sol +++ b/contracts/lib/Errors.sol @@ -435,6 +435,68 @@ library Errors { /// @notice Zero arbitration policy cooldown provided. error DisputeModule__ZeroArbitrationPolicyCooldown(); + //////////////////////////////////////////////////////////////////////////// + // Arbitration Policy UMA // + //////////////////////////////////////////////////////////////////////////// + + /// @notice Only dispute module can call. + error ArbitrationPolicyUMA__NotDisputeModule(); + + /// @notice Zero address provided for Dispute Module. + error ArbitrationPolicyUMA__ZeroDisputeModule(); + + /// @notice Zero address provided for OOV3. + error ArbitrationPolicyUMA__ZeroOOV3(); + + /// @notice Zero address provided for Access Manager. + error ArbitrationPolicyUMA__ZeroAccessManager(); + + /// @notice Zero min liveness provided. + error ArbitrationPolicyUMA__ZeroMinLiveness(); + + /// @notice Zero max liveness provided. + error ArbitrationPolicyUMA__ZeroMaxLiveness(); + + /// @notice Liveness is too short. + error ArbitrationPolicyUMA__LivenessBelowMin(); + + /// @notice Liveness is too long. + error ArbitrationPolicyUMA__LivenessAboveMax(); + + /// @notice Min liveness is above max liveness. + error ArbitrationPolicyUMA__MinLivenessAboveMax(); + + /// @notice IP owner time percent is above max. + error ArbitrationPolicyUMA__IpOwnerTimePercentAboveMax(); + + /// @notice Bond size is above max. + error ArbitrationPolicyUMA__BondAboveMax(); + + /// @notice Cannot cancel. + error ArbitrationPolicyUMA__CannotCancel(); + + /// @notice Only OOV3 can call. + error ArbitrationPolicyUMA__NotOOV3(); + + /// @notice No counter evidence provided. + error ArbitrationPolicyUMA__NoCounterEvidence(); + + /// @notice Dispute not found. + error ArbitrationPolicyUMA__DisputeNotFound(); + + /// @notice Cannot dispute assertion if tag is inherited. + error ArbitrationPolicyUMA__CannotDisputeAssertionIfTagIsInherited(); + + /// @notice Only target IP id can dispute within time window. + error ArbitrationPolicyUMA__OnlyTargetIpIdCanDisputeWithinTimeWindow( + uint64 elapsedTime, + uint64 liveness, + address caller + ); + + /// @notice Not the UMA dispute policy. + error ArbitrationPolicyUMA__OnlyDisputePolicyUMA(); + //////////////////////////////////////////////////////////////////////////// // Royalty Module // //////////////////////////////////////////////////////////////////////////// diff --git a/contracts/modules/dispute/DisputeModule.sol b/contracts/modules/dispute/DisputeModule.sol index 8fe3bb94..1711b3cf 100644 --- a/contracts/modules/dispute/DisputeModule.sol +++ b/contracts/modules/dispute/DisputeModule.sol @@ -206,10 +206,12 @@ contract DisputeModule is address arbitrationPolicy = _updateActiveArbitrationPolicy(targetIpId); uint256 disputeId = ++$.disputeCounter; + uint256 disputeTimestamp = block.timestamp; $.disputes[disputeId] = Dispute({ targetIpId: targetIpId, disputeInitiator: msg.sender, + disputeTimestamp: disputeTimestamp, arbitrationPolicy: arbitrationPolicy, disputeEvidenceHash: disputeEvidenceHash, targetTag: targetTag, @@ -219,7 +221,16 @@ contract DisputeModule is IArbitrationPolicy(arbitrationPolicy).onRaiseDispute(msg.sender, data); - emit DisputeRaised(disputeId, targetIpId, msg.sender, arbitrationPolicy, disputeEvidenceHash, targetTag, data); + emit DisputeRaised( + disputeId, + targetIpId, + msg.sender, + disputeTimestamp, + arbitrationPolicy, + disputeEvidenceHash, + targetTag, + data + ); return disputeId; } @@ -296,10 +307,12 @@ contract DisputeModule is if (!$.isWhitelistedArbitrationPolicy[arbitrationPolicy]) arbitrationPolicy = $.baseArbitrationPolicy; uint256 disputeId = ++$.disputeCounter; + uint256 disputeTimestamp = block.timestamp; $.disputes[disputeId] = Dispute({ targetIpId: derivativeIpId, disputeInitiator: msg.sender, + disputeTimestamp: disputeTimestamp, arbitrationPolicy: arbitrationPolicy, disputeEvidenceHash: "", targetTag: parentDispute.currentTag, @@ -313,7 +326,8 @@ contract DisputeModule is parentIpId, derivativeIpId, parentDisputeId, - parentDispute.currentTag + parentDispute.currentTag, + disputeTimestamp ); } @@ -341,7 +355,7 @@ contract DisputeModule is IArbitrationPolicy(dispute.arbitrationPolicy).onResolveDispute(msg.sender, disputeId, data); - emit DisputeResolved(disputeId); + emit DisputeResolved(disputeId, data); } /// @notice Updates the active arbitration policy for a given ipId @@ -377,6 +391,7 @@ contract DisputeModule is /// @param disputeId The dispute id /// @return targetIpId The ipId that is the target of the dispute /// @return disputeInitiator The address of the dispute initiator + /// @return disputeTimestamp The timestamp of the dispute /// @return arbitrationPolicy The address of the arbitration policy /// @return disputeEvidenceHash The hash pointing to the dispute evidence /// @return targetTag The target tag of the dispute @@ -390,6 +405,7 @@ contract DisputeModule is returns ( address targetIpId, address disputeInitiator, + uint256 disputeTimestamp, address arbitrationPolicy, bytes32 disputeEvidenceHash, bytes32 targetTag, @@ -401,6 +417,7 @@ contract DisputeModule is return ( dispute.targetIpId, dispute.disputeInitiator, + dispute.disputeTimestamp, dispute.arbitrationPolicy, dispute.disputeEvidenceHash, dispute.targetTag, diff --git a/contracts/modules/dispute/policies/UMA/ArbitrationPolicyUMA.sol b/contracts/modules/dispute/policies/UMA/ArbitrationPolicyUMA.sol new file mode 100644 index 00000000..a7f523f2 --- /dev/null +++ b/contracts/modules/dispute/policies/UMA/ArbitrationPolicyUMA.sol @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +import { IDisputeModule } from "../../../../interfaces/modules/dispute/IDisputeModule.sol"; +import { IArbitrationPolicyUMA } from "../../../../interfaces/modules/dispute/policies/UMA/IArbitrationPolicyUMA.sol"; +import { IOOV3 } from "../../../../interfaces/modules/dispute/policies/UMA/IOOV3.sol"; +import { ProtocolPausableUpgradeable } from "../../../../pause/ProtocolPausableUpgradeable.sol"; +import { Errors } from "../../../../lib/Errors.sol"; + +/// @title Arbitration Policy UMA +/// @notice The arbitration policy UMA acts as an enforcement layer for IP assets that allows raising and judging +/// disputes according to the UMA protocol rules. +contract ArbitrationPolicyUMA is + IArbitrationPolicyUMA, + ProtocolPausableUpgradeable, + ReentrancyGuardUpgradeable, + UUPSUpgradeable +{ + using SafeERC20 for IERC20; + + /// @notice Returns the percentage scale - represents 100% + uint32 public constant MAX_PERCENT = 100_000_000; + + /// @notice Dispute module address + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + address public immutable DISPUTE_MODULE; + + /// @notice OOV3 address + /// @custom:oz-upgrades-unsafe-allow state-variable-immutable + IOOV3 public immutable OOV3; + + /// @dev Storage structure for the ArbitrationPolicyUMA + /// @param minLiveness The minimum liveness value + /// @param maxLiveness The maximum liveness value + /// @param ipOwnerTimePercent The percentage of liveness time the IP owner has priority to respond to a dispute + /// @param maxBonds The maximum bond size for each token + /// @param disputeIdToAssertionId The mapping of dispute id to assertion id + /// @param assertionIdToDisputeId The mapping of assertion id to dispute id + /// @param counterEvidenceHashes The mapping of assertion id to counter evidence hash + /// @custom:storage-location erc7201:story-protocol.ArbitrationPolicyUMA + struct ArbitrationPolicyUMAStorage { + uint64 minLiveness; + uint64 maxLiveness; + uint32 ipOwnerTimePercent; + mapping(address token => uint256 maxBondSize) maxBonds; + mapping(uint256 disputeId => bytes32 assertionId) disputeIdToAssertionId; + mapping(bytes32 assertionId => uint256 disputeId) assertionIdToDisputeId; + mapping(bytes32 assertionId => bytes32 counterEvidenceHash) counterEvidenceHashes; + } + + // keccak256(abi.encode(uint256(keccak256("story-protocol.ArbitrationPolicyUMA")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant ArbitrationPolicyUMAStorageLocation = + 0xbd39630b628d883a3167c4982acf741cbddb24bae6947600210f8eb1db515300; + + /// @dev Restricts the calls to the dispute module + modifier onlyDisputeModule() { + if (msg.sender != DISPUTE_MODULE) revert Errors.ArbitrationPolicyUMA__NotDisputeModule(); + _; + } + + /// Constructor + /// @param disputeModule The address of the dispute module + /// @param oov3 The address of the OOV3 + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address disputeModule, address oov3) { + if (disputeModule == address(0)) revert Errors.ArbitrationPolicyUMA__ZeroDisputeModule(); + if (oov3 == address(0)) revert Errors.ArbitrationPolicyUMA__ZeroOOV3(); + + DISPUTE_MODULE = disputeModule; + OOV3 = IOOV3(oov3); + _disableInitializers(); + } + + /// @notice Initializer for this implementation contract + /// @param accessManager The address of the protocol admin roles contract + function initialize(address accessManager) external initializer { + if (accessManager == address(0)) revert Errors.ArbitrationPolicyUMA__ZeroAccessManager(); + + __ProtocolPausable_init(accessManager); + __ReentrancyGuard_init(); + __UUPSUpgradeable_init(); + } + + /// @notice Sets the liveness for UMA disputes + /// @param minLiveness The minimum liveness value + /// @param maxLiveness The maximum liveness value + /// @param ipOwnerTimePercent The percentage of liveness time the IP owner has priority to respond to a dispute + function setLiveness(uint64 minLiveness, uint64 maxLiveness, uint32 ipOwnerTimePercent) external restricted { + if (minLiveness == 0) revert Errors.ArbitrationPolicyUMA__ZeroMinLiveness(); + if (maxLiveness == 0) revert Errors.ArbitrationPolicyUMA__ZeroMaxLiveness(); + if (minLiveness > maxLiveness) revert Errors.ArbitrationPolicyUMA__MinLivenessAboveMax(); + if (ipOwnerTimePercent > MAX_PERCENT) revert Errors.ArbitrationPolicyUMA__IpOwnerTimePercentAboveMax(); + + ArbitrationPolicyUMAStorage storage $ = _getArbitrationPolicyUMAStorage(); + $.minLiveness = minLiveness; + $.maxLiveness = maxLiveness; + $.ipOwnerTimePercent = ipOwnerTimePercent; + + emit LivenessSet(minLiveness, maxLiveness, ipOwnerTimePercent); + } + + /// @notice Sets the max bond for UMA disputes + /// @param token The token address + /// @param maxBond The maximum bond value + function setMaxBond(address token, uint256 maxBond) external restricted { + ArbitrationPolicyUMAStorage storage $ = _getArbitrationPolicyUMAStorage(); + $.maxBonds[token] = maxBond; + + emit MaxBondSet(token, maxBond); + } + + /// @notice Executes custom logic on raising dispute + /// @dev Enforced to be only callable by the DisputeModule + /// @param caller Address of the caller + /// @param data The arbitrary data used to raise the dispute + function onRaiseDispute(address caller, bytes calldata data) external onlyDisputeModule nonReentrant { + (bytes memory claim, uint64 liveness, address currency, uint256 bond, bytes32 identifier) = abi.decode( + data, + (bytes, uint64, address, uint256, bytes32) + ); + + ArbitrationPolicyUMAStorage storage $ = _getArbitrationPolicyUMAStorage(); + if (liveness < $.minLiveness) revert Errors.ArbitrationPolicyUMA__LivenessBelowMin(); + if (liveness > $.maxLiveness) revert Errors.ArbitrationPolicyUMA__LivenessAboveMax(); + if (bond > $.maxBonds[currency]) revert Errors.ArbitrationPolicyUMA__BondAboveMax(); + + IERC20 currencyToken = IERC20(currency); + currencyToken.safeTransferFrom(caller, address(this), bond); + currencyToken.safeIncreaseAllowance(address(OOV3), bond); + + bytes32 assertionId = OOV3.assertTruth( + claim, + caller, // asserter + address(this), // callbackRecipient + address(0), // escalationManager + liveness, + currencyToken, + bond, + identifier, + bytes32(0) // domainId + ); + + uint256 disputeId = IDisputeModule(DISPUTE_MODULE).disputeCounter(); + $.assertionIdToDisputeId[assertionId] = disputeId; + $.disputeIdToAssertionId[disputeId] = assertionId; + + emit DisputeRaisedUMA(disputeId, caller, claim, liveness, currency, bond, identifier); + } + + /// @notice Executes custom logic on disputing judgement + /// @dev Enforced to be only callable by the DisputeModule. For UMA arbitration, no custom logic is required. + /// @param disputeId The dispute id + /// @param decision The decision of the dispute + /// @param data The arbitrary data used to set the dispute judgement + function onDisputeJudgement(uint256 disputeId, bool decision, bytes calldata data) external onlyDisputeModule {} + + /// @notice Executes custom logic on disputing cancel + /// @dev Enforced to be only callable by the DisputeModule + /// @param caller Address of the caller + /// @param disputeId The dispute id + /// @param data The arbitrary data used to cancel the dispute + function onDisputeCancel(address caller, uint256 disputeId, bytes calldata data) external onlyDisputeModule { + revert Errors.ArbitrationPolicyUMA__CannotCancel(); + } + + /// @notice Executes custom logic on resolving dispute + /// @dev Enforced to be only callable by the DisputeModule. For UMA arbitration, no custom logic is required. + /// @param caller Address of the caller + /// @param disputeId The dispute id + /// @param data The arbitrary data used to resolve the dispute + function onResolveDispute(address caller, uint256 disputeId, bytes calldata data) external onlyDisputeModule {} + + /// @notice Allows the IP that was targeted to dispute the assertion while providing counter evidence + /// @param assertionId The identifier of the assertion that was disputed + /// @param counterEvidenceHash The hash of the counter evidence + function disputeAssertion(bytes32 assertionId, bytes32 counterEvidenceHash) external nonReentrant { + if (counterEvidenceHash == bytes32(0)) revert Errors.ArbitrationPolicyUMA__NoCounterEvidence(); + + ArbitrationPolicyUMAStorage storage $ = _getArbitrationPolicyUMAStorage(); + uint256 disputeId = $.assertionIdToDisputeId[assertionId]; + if (disputeId == 0) revert Errors.ArbitrationPolicyUMA__DisputeNotFound(); + + (address targetIpId, , , address arbitrationPolicy, , , , uint256 parentDisputeId) = IDisputeModule( + DISPUTE_MODULE + ).disputes(disputeId); + + if (arbitrationPolicy != address(this)) revert Errors.ArbitrationPolicyUMA__OnlyDisputePolicyUMA(); + if (parentDisputeId > 0) revert Errors.ArbitrationPolicyUMA__CannotDisputeAssertionIfTagIsInherited(); + + // Check if the address can dispute the assertion depending on the liveness and the elapsed time + IOOV3.Assertion memory assertion = OOV3.getAssertion(assertionId); + uint64 liveness = assertion.expirationTime - assertion.assertionTime; + uint64 elapsedTime = uint64(block.timestamp) - assertion.assertionTime; + bool inIpOwnerTimeWindow = elapsedTime <= (liveness * $.ipOwnerTimePercent) / MAX_PERCENT; + if (inIpOwnerTimeWindow && msg.sender != targetIpId) + revert Errors.ArbitrationPolicyUMA__OnlyTargetIpIdCanDisputeWithinTimeWindow( + elapsedTime, + liveness, + msg.sender + ); + + $.counterEvidenceHashes[assertionId] = counterEvidenceHash; + + IERC20 currencyToken = IERC20(assertion.currency); + currencyToken.safeTransferFrom(msg.sender, address(this), assertion.bond); + currencyToken.safeIncreaseAllowance(address(OOV3), assertion.bond); + + OOV3.disputeAssertion(assertionId, msg.sender); + + emit AssertionDisputed(assertionId, counterEvidenceHash); + } + + /// @notice OOV3 callback function forwhen an assertion is resolved + /// @param assertionId The resolved assertion identifier + /// @param assertedTruthfully Indicates if the assertion was resolved as truthful or not + function assertionResolvedCallback(bytes32 assertionId, bool assertedTruthfully) external nonReentrant { + if (msg.sender != address(OOV3)) revert Errors.ArbitrationPolicyUMA__NotOOV3(); + + ArbitrationPolicyUMAStorage storage $ = _getArbitrationPolicyUMAStorage(); + uint256 disputeId = $.assertionIdToDisputeId[assertionId]; + + IDisputeModule(DISPUTE_MODULE).setDisputeJudgement(disputeId, assertedTruthfully, ""); + } + + /// @notice OOV3 callback function for when an assertion is disputed + /// @param assertionId The disputed assertion identifier + function assertionDisputedCallback(bytes32 assertionId) external { + if (msg.sender != address(OOV3)) revert Errors.ArbitrationPolicyUMA__NotOOV3(); + + ArbitrationPolicyUMAStorage storage $ = _getArbitrationPolicyUMAStorage(); + if ($.counterEvidenceHashes[assertionId] == bytes32(0)) revert Errors.ArbitrationPolicyUMA__NoCounterEvidence(); + } + + /// @notice Returns the maximum percentage - represents 100% + function maxPercent() external view returns (uint32) { + return MAX_PERCENT; + } + + /// @notice Returns the minimum liveness for UMA disputes + function minLiveness() external view returns (uint64) { + return _getArbitrationPolicyUMAStorage().minLiveness; + } + + /// @notice Returns the maximum liveness for UMA disputes + function maxLiveness() external view returns (uint64) { + return _getArbitrationPolicyUMAStorage().maxLiveness; + } + + /// @notice Returns the percentage of liveness time the IP owner has priority to respond to a dispute + function ipOwnerTimePercent() external view returns (uint32) { + return _getArbitrationPolicyUMAStorage().ipOwnerTimePercent; + } + + /// @notice Returns the maximum bond for a given token for UMA disputes + /// @param token The token address + function maxBonds(address token) external view returns (uint256) { + return _getArbitrationPolicyUMAStorage().maxBonds[token]; + } + + /// @notice Returns the assertion id for a given dispute id + /// @param disputeId The dispute id + function disputeIdToAssertionId(uint256 disputeId) external view returns (bytes32) { + return _getArbitrationPolicyUMAStorage().disputeIdToAssertionId[disputeId]; + } + + /// @notice Returns the dispute id for a given assertion id + /// @param assertionId The assertion id + function assertionIdToDisputeId(bytes32 assertionId) external view returns (uint256) { + return _getArbitrationPolicyUMAStorage().assertionIdToDisputeId[assertionId]; + } + + /// @dev Hook to authorize the upgrade according to UUPSUpgradeable + /// @param newImplementation The address of the new implementation + function _authorizeUpgrade(address newImplementation) internal override restricted {} + + /// @dev Returns the storage struct of ArbitrationPolicyUMA + function _getArbitrationPolicyUMAStorage() private pure returns (ArbitrationPolicyUMAStorage storage $) { + assembly { + $.slot := ArbitrationPolicyUMAStorageLocation + } + } +} diff --git a/script/foundry/utils/DeployHelper.sol b/script/foundry/utils/DeployHelper.sol index e72b3d52..dbabec65 100644 --- a/script/foundry/utils/DeployHelper.sol +++ b/script/foundry/utils/DeployHelper.sol @@ -36,6 +36,7 @@ import { RoyaltyPolicyLAP } from "contracts/modules/royalty/policies/LAP/Royalty import { RoyaltyPolicyLRP } from "contracts/modules/royalty/policies/LRP/RoyaltyPolicyLRP.sol"; import { VaultController } from "contracts/modules/royalty/policies/VaultController.sol"; import { DisputeModule } from "contracts/modules/dispute/DisputeModule.sol"; +import { ArbitrationPolicyUMA } from "contracts/modules/dispute/policies/UMA/ArbitrationPolicyUMA.sol"; import { MODULE_TYPE_HOOK } from "contracts/lib/modules/Module.sol"; import { IModule } from "contracts/interfaces/modules/base/IModule.sol"; import { IHookModule } from "contracts/interfaces/modules/base/IHookModule.sol"; @@ -90,6 +91,8 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag CoreMetadataViewModule internal coreMetadataViewModule; // Policy + ArbitrationPolicyUMA internal arbitrationPolicyUMA; + address internal oov3; RoyaltyPolicyLAP internal royaltyPolicyLAP; RoyaltyPolicyLRP internal royaltyPolicyLRP; UpgradeableBeacon internal ipRoyaltyVaultBeacon; @@ -138,17 +141,19 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag erc6551Registry = ERC6551Registry(erc6551Registry_); create3Deployer = ICreate3Deployer(create3Deployer_); erc20 = ERC20(erc20_); - ARBITRATION_PRICE = arbitrationPrice_; MAX_ROYALTY_APPROVAL = maxRoyaltyApproval_; TREASURY_ADDRESS = treasury_; ipGraphACL = IPGraphACL(ipGraphACL_); - + oov3 = address(1); // mock address replaced below depending on chainid /// @dev USDC addresses are fetched from /// (mainnet) https://developers.circle.com/stablecoins/docs/usdc-on-main-networks /// (testnet) https://developers.circle.com/stablecoins/docs/usdc-on-test-networks if (block.chainid == 1) erc20 = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); else if (block.chainid == 11155111) erc20 = ERC20(0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238); - else if (block.chainid == 1513) erc20 = ERC20(0x91f6F05B08c16769d3c85867548615d270C42fC7); + else if (block.chainid == 1513) { + erc20 = ERC20(0x91f6F05B08c16769d3c85867548615d270C42fC7); + oov3 = 0x3CA11702f7c0F28e0b4e03C31F7492969862C569; + } } /// @dev To use, run the following command (e.g. for Sepolia): @@ -514,6 +519,27 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag // Story-specific Non-Core Contracts // + _predeploy("ArbitrationPolicyUMA"); + impl = address(new ArbitrationPolicyUMA(address(disputeModule), oov3)); + arbitrationPolicyUMA = ArbitrationPolicyUMA( + TestProxyHelper.deployUUPSProxy( + create3Deployer, + _getSalt(type(ArbitrationPolicyUMA).name), + impl, + abi.encodeCall(ArbitrationPolicyUMA.initialize, address(protocolAccessManager)) + ) + ); + require( + _getDeployedAddress(type(ArbitrationPolicyUMA).name) == address(arbitrationPolicyUMA), + "Deploy: Arbitration Policy Address Mismatch" + ); + require( + _loadProxyImpl(address(arbitrationPolicyUMA)) == impl, + "ArbitrationPolicyUMA Proxy Implementation Mismatch" + ); + impl = address(0); + _postdeploy("ArbitrationPolicyUMA", address(arbitrationPolicyUMA)); + _predeploy("RoyaltyPolicyLAP"); impl = address(new RoyaltyPolicyLAP( address(royaltyModule), @@ -722,8 +748,13 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag royaltyModule.setIpRoyaltyVaultBeacon(address(ipRoyaltyVaultBeacon)); ipRoyaltyVaultBeacon.transferOwnership(address(royaltyModule)); - // Dispute Module and SP Dispute Policy + // Dispute Module and Dispute Policy disputeModule.whitelistDisputeTag("PLAGIARISM", true); + disputeModule.whitelistArbitrationPolicy(address(arbitrationPolicyUMA), true); + disputeModule.whitelistArbitrationRelayer(address(arbitrationPolicyUMA), address(arbitrationPolicyUMA), true); + disputeModule.setBaseArbitrationPolicy(address(arbitrationPolicyUMA)); + arbitrationPolicyUMA.setLiveness(30 days, 365 days, 66_666_666); + arbitrationPolicyUMA.setMaxBond(address(erc20), 25000e18); // 25k USD max bond disputeModule.setArbitrationPolicyCooldown(7 days); // Core Metadata Module @@ -759,6 +790,11 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag protocolAccessManager.setTargetFunctionRole(address(licenseToken), selectors, ProtocolAdmin.UPGRADER_ROLE); protocolAccessManager.setTargetFunctionRole(address(accessController), selectors, ProtocolAdmin.UPGRADER_ROLE); protocolAccessManager.setTargetFunctionRole(address(disputeModule), selectors, ProtocolAdmin.UPGRADER_ROLE); + protocolAccessManager.setTargetFunctionRole( + address(arbitrationPolicyUMA), + selectors, + ProtocolAdmin.UPGRADER_ROLE + ); protocolAccessManager.setTargetFunctionRole(address(licensingModule), selectors, ProtocolAdmin.UPGRADER_ROLE); protocolAccessManager.setTargetFunctionRole(address(royaltyPolicyLAP), selectors, ProtocolAdmin.UPGRADER_ROLE); protocolAccessManager.setTargetFunctionRole(address(royaltyPolicyLRP), selectors, ProtocolAdmin.UPGRADER_ROLE); @@ -851,7 +887,7 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag } /// @dev get the salt for the contract deployment with CREATE3 - function _getSalt(string memory name) private view returns (bytes32 salt) { + function _getSalt(string memory name) internal view returns (bytes32 salt) { salt = keccak256(abi.encode(name, create3SaltSeed)); } diff --git a/test/foundry/invariants/DisputeModule.t.sol b/test/foundry/invariants/DisputeModule.t.sol index 14c5af02..35c8fde3 100644 --- a/test/foundry/invariants/DisputeModule.t.sol +++ b/test/foundry/invariants/DisputeModule.t.sol @@ -208,6 +208,7 @@ contract DisputeInvariants is BaseTest { ( address targetIpId, address disputeInitiator, + uint256 disputeTimestamp, address arbitrationPolicy, bytes32 _disputeEvidenceHash, bytes32 targetTag, diff --git a/test/foundry/mocks/IMockAncillary.sol b/test/foundry/mocks/IMockAncillary.sol new file mode 100644 index 00000000..547401a0 --- /dev/null +++ b/test/foundry/mocks/IMockAncillary.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +/// @title Mock Ancillary Interface +interface IMockAncillary { + function requestPrice(bytes32 identifier, uint256 time, bytes memory ancillaryData) external; + + function pushPrice(bytes32 identifier, uint256 time, bytes memory ancillaryData, int256 price) external; +} diff --git a/test/foundry/mocks/dispute/MockArbitrationPolicy.sol b/test/foundry/mocks/dispute/MockArbitrationPolicy.sol index 2771ca5b..eda52cfa 100644 --- a/test/foundry/mocks/dispute/MockArbitrationPolicy.sol +++ b/test/foundry/mocks/dispute/MockArbitrationPolicy.sol @@ -40,7 +40,7 @@ contract MockArbitrationPolicy is IArbitrationPolicy { function onDisputeJudgement(uint256 disputeId, bool decision, bytes calldata data) external onlyDisputeModule { if (decision) { - (, address disputeInitiator, , , , , ) = IDisputeModule(DISPUTE_MODULE).disputes(disputeId); + (, address disputeInitiator, , , , , , ) = IDisputeModule(DISPUTE_MODULE).disputes(disputeId); IERC20(PAYMENT_TOKEN).safeTransfer(disputeInitiator, ARBITRATION_PRICE); } else { IERC20(PAYMENT_TOKEN).safeTransfer(treasury, ARBITRATION_PRICE); diff --git a/test/foundry/mocks/dispute/MockIpAssetRegistry.sol b/test/foundry/mocks/dispute/MockIpAssetRegistry.sol new file mode 100644 index 00000000..166fa243 --- /dev/null +++ b/test/foundry/mocks/dispute/MockIpAssetRegistry.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +contract MockIpAssetRegistry { + function isRegistered(address ipId) external view returns (bool) { + return true; + } +} diff --git a/test/foundry/modules/dispute/DisputeModule.t.sol b/test/foundry/modules/dispute/DisputeModule.t.sol index 1c4ebe9c..561e21e8 100644 --- a/test/foundry/modules/dispute/DisputeModule.t.sol +++ b/test/foundry/modules/dispute/DisputeModule.t.sol @@ -21,6 +21,7 @@ contract DisputeModuleTest is BaseTest { uint256 disputeId, address targetIpId, address disputeInitiator, + uint256 disputeTimestamp, address arbitrationPolicy, bytes32 disputeEvidenceHash, bytes32 targetTag, @@ -28,7 +29,7 @@ contract DisputeModuleTest is BaseTest { ); event DisputeJudgementSet(uint256 disputeId, bool decision, bytes data); event DisputeCancelled(uint256 disputeId, bytes data); - event DisputeResolved(uint256 disputeId); + event DisputeResolved(uint256 disputeId, bytes data); event DefaultArbitrationPolicyUpdated(address arbitrationPolicy); address internal ipAccount1 = address(0x111000aaa); @@ -292,6 +293,7 @@ contract DisputeModuleTest is BaseTest { disputeIdBefore + 1, ipAddr, ipAccount1, + block.timestamp, address(mockArbitrationPolicy2), disputeEvidenceHashExample, bytes32("PLAGIARISM"), @@ -307,6 +309,7 @@ contract DisputeModuleTest is BaseTest { ( address targetIpId, address disputeInitiator, + uint256 disputeTimestamp, address arbitrationPolicy, bytes32 disputeEvidenceHash, bytes32 targetTag, @@ -320,6 +323,7 @@ contract DisputeModuleTest is BaseTest { assertEq(mockArbitrationPolicyUSDCBalanceAfter - mockArbitrationPolicyUSDCBalanceBefore, ARBITRATION_PRICE); assertEq(targetIpId, ipAddr); assertEq(disputeInitiator, ipAccount1); + assertEq(disputeTimestamp, block.timestamp); assertEq(arbitrationPolicy, address(mockArbitrationPolicy2)); assertEq(disputeEvidenceHash, disputeEvidenceHashExample); assertEq(targetTag, bytes32("PLAGIARISM")); @@ -340,6 +344,7 @@ contract DisputeModuleTest is BaseTest { disputeIdBefore + 1, ipAddr, ipAccount1, + block.timestamp, address(mockArbitrationPolicy), disputeEvidenceHashExample, bytes32("PLAGIARISM"), @@ -355,6 +360,7 @@ contract DisputeModuleTest is BaseTest { ( address targetIpId, address disputeInitiator, + uint256 disputeTimestamp, address arbitrationPolicy, bytes32 disputeEvidenceHash, bytes32 targetTag, @@ -367,6 +373,7 @@ contract DisputeModuleTest is BaseTest { assertEq(mockArbitrationPolicyUSDCBalanceAfter - mockArbitrationPolicyUSDCBalanceBefore, ARBITRATION_PRICE); assertEq(targetIpId, ipAddr); assertEq(disputeInitiator, ipAccount1); + assertEq(disputeTimestamp, block.timestamp); assertEq(arbitrationPolicy, address(mockArbitrationPolicy)); assertEq(disputeEvidenceHash, disputeEvidenceHashExample); assertEq(targetTag, bytes32("PLAGIARISM")); @@ -398,7 +405,7 @@ contract DisputeModuleTest is BaseTest { vm.stopPrank(); // set dispute judgement - (, , , , , bytes32 currentTagBefore, ) = disputeModule.disputes(1); + (, , , , , , bytes32 currentTagBefore, ) = disputeModule.disputes(1); uint256 ipAccount1USDCBalanceBefore = USDC.balanceOf(ipAccount1); uint256 mockArbitrationPolicyUSDCBalanceBefore = USDC.balanceOf(address(mockArbitrationPolicy)); @@ -408,7 +415,7 @@ contract DisputeModuleTest is BaseTest { vm.startPrank(arbitrationRelayer); disputeModule.setDisputeJudgement(1, true, ""); - (, , , , , bytes32 currentTagAfter, ) = disputeModule.disputes(1); + (, , , , , , bytes32 currentTagAfter, ) = disputeModule.disputes(1); uint256 ipAccount1USDCBalanceAfter = USDC.balanceOf(ipAccount1); uint256 mockArbitrationPolicyUSDCBalanceAfter = USDC.balanceOf(address(mockArbitrationPolicy)); @@ -427,7 +434,7 @@ contract DisputeModuleTest is BaseTest { vm.stopPrank(); // set dispute judgement - (, , , , , bytes32 currentTagBefore, ) = disputeModule.disputes(1); + (, , , , , , bytes32 currentTagBefore, ) = disputeModule.disputes(1); uint256 ipAccount1USDCBalanceBefore = USDC.balanceOf(ipAccount1); uint256 mockArbitrationPolicyUSDCBalanceBefore = USDC.balanceOf(address(mockArbitrationPolicy)); @@ -437,7 +444,7 @@ contract DisputeModuleTest is BaseTest { vm.startPrank(arbitrationRelayer); disputeModule.setDisputeJudgement(1, false, ""); - (, , , , , bytes32 currentTagAfter, ) = disputeModule.disputes(1); + (, , , , , , bytes32 currentTagAfter, ) = disputeModule.disputes(1); uint256 ipAccount1USDCBalanceAfter = USDC.balanceOf(ipAccount1); uint256 mockArbitrationPolicyUSDCBalanceAfter = USDC.balanceOf(address(mockArbitrationPolicy)); @@ -487,7 +494,7 @@ contract DisputeModuleTest is BaseTest { disputeModule.raiseDispute(ipAddr, disputeEvidenceHashExample, "PLAGIARISM", ""); vm.stopPrank(); - (, , , , , bytes32 currentTagBeforeCancel, ) = disputeModule.disputes(1); + (, , , , , , bytes32 currentTagBeforeCancel, ) = disputeModule.disputes(1); vm.startPrank(ipAccount1); vm.expectEmit(true, true, true, true, address(disputeModule)); @@ -496,7 +503,7 @@ contract DisputeModuleTest is BaseTest { disputeModule.cancelDispute(1, ""); vm.stopPrank(); - (, , , , , bytes32 currentTagAfterCancel, ) = disputeModule.disputes(1); + (, , , , , , bytes32 currentTagAfterCancel, ) = disputeModule.disputes(1); assertEq(currentTagBeforeCancel, bytes32("IN_DISPUTE")); assertEq(currentTagAfterCancel, bytes32(0)); @@ -560,7 +567,7 @@ contract DisputeModuleTest is BaseTest { // tag child ip vm.startPrank(address(1)); vm.expectEmit(true, true, true, true, address(disputeModule)); - emit IDisputeModule.DerivativeTaggedOnParentInfringement(ipAddr, ipAddr2, 1, "PLAGIARISM"); + emit IDisputeModule.DerivativeTaggedOnParentInfringement(ipAddr, ipAddr2, 1, "PLAGIARISM", block.timestamp); uint256 disputeIdBefore = disputeModule.disputeCounter(); @@ -571,6 +578,7 @@ contract DisputeModuleTest is BaseTest { ( address targetIpId, address disputeInitiator, + uint256 disputeTimestamp, address arbitrationPolicy, bytes32 disputeEvidenceHash, bytes32 targetTag, @@ -583,6 +591,7 @@ contract DisputeModuleTest is BaseTest { assertTrue(disputeModule.isIpTagged(ipAddr2)); assertEq(targetIpId, ipAddr2); assertEq(disputeInitiator, address(1)); + assertEq(disputeTimestamp, block.timestamp); assertEq(arbitrationPolicy, address(mockArbitrationPolicy)); assertEq(disputeEvidenceHash, bytes32(0)); assertEq(targetTag, bytes32("PLAGIARISM")); @@ -638,16 +647,16 @@ contract DisputeModuleTest is BaseTest { disputeModule.setDisputeJudgement(1, true, ""); vm.stopPrank(); - (, , , , , bytes32 currentTagBeforeResolve, ) = disputeModule.disputes(1); + (, , , , , , bytes32 currentTagBeforeResolve, ) = disputeModule.disputes(1); // resolve dispute vm.startPrank(ipAccount1); vm.expectEmit(true, true, true, true, address(disputeModule)); - emit DisputeResolved(1); + emit DisputeResolved(1, ""); disputeModule.resolveDispute(1, ""); - (, , , , , bytes32 currentTagAfterResolve, ) = disputeModule.disputes(1); + (, , , , , , bytes32 currentTagAfterResolve, ) = disputeModule.disputes(1); assertEq(currentTagBeforeResolve, bytes32("PLAGIARISM")); assertEq(currentTagAfterResolve, bytes32(0)); diff --git a/test/foundry/modules/dispute/policies/UMA/ArbitrationPolicyUMA.t.sol b/test/foundry/modules/dispute/policies/UMA/ArbitrationPolicyUMA.t.sol new file mode 100644 index 00000000..784558d2 --- /dev/null +++ b/test/foundry/modules/dispute/policies/UMA/ArbitrationPolicyUMA.t.sol @@ -0,0 +1,705 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.26; + +import { AccessManager } from "@openzeppelin/contracts/access/manager/AccessManager.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import { DisputeModule } from "contracts/modules/dispute/DisputeModule.sol"; +import { ArbitrationPolicyUMA } from "contracts/modules/dispute/policies/UMA/ArbitrationPolicyUMA.sol"; +import { IOOV3 } from "contracts/interfaces/modules/dispute/policies/UMA/IOOV3.sol"; +import { Errors } from "contracts/lib/Errors.sol"; + +import { BaseTest } from "test/foundry/utils/BaseTest.t.sol"; +import { MockIpAssetRegistry } from "test/foundry/mocks/dispute/MockIpAssetRegistry.sol"; +import { IMockAncillary } from "test/foundry/mocks/IMockAncillary.sol"; +import { MockERC20 } from "test/foundry/mocks/token/MockERC20.sol"; +import { TestProxyHelper } from "test/foundry/utils/TestProxyHelper.sol"; + +contract ArbitrationPolicyUMATest is BaseTest { + event LivenessSet(uint64 minLiveness, uint64 maxLiveness, uint32 ipOwnerTimePercent); + event MaxBondSet(address token, uint256 maxBond); + event DisputeRaisedUMA( + uint256 disputeId, + address caller, + bytes claim, + uint64 liveness, + address currency, + uint256 bond, + bytes32 identifier + ); + event AssertionDisputed(bytes32 assertionId, bytes32 counterEvidenceHash); + + MockIpAssetRegistry mockIpAssetRegistry; + ArbitrationPolicyUMA newArbitrationPolicyUMA; + DisputeModule newDisputeModule; + address internal newOOV3; + AccessManager newAccessManager; + address internal newAdmin; + address internal susd; + address internal mockAncillary; + bytes32 internal disputeEvidenceHashExample = 0xb7b94ecbd1f9f8cb209909e5785fb2858c9a8c4b220c017995a75346ad1b5db5; + + function setUp() public virtual override { + // Fork the desired network where UMA contracts are deployed + uint256 forkId = vm.createFork("https://testnet.storyrpc.io"); + vm.selectFork(forkId); + + // Illiad chain 1513 + newOOV3 = 0x3CA11702f7c0F28e0b4e03C31F7492969862C569; + mockAncillary = 0x3baD7AD0728f9917d1Bf08af5782dCbD516cDd96; + susd = 0x91f6F05B08c16769d3c85867548615d270C42fC7; + + // deploy mock ip asset registry + mockIpAssetRegistry = new MockIpAssetRegistry(); + + // deploy access manager + newAdmin = address(100); + newAccessManager = new AccessManager(newAdmin); + + vm.startPrank(newAdmin); + + // deploy dispute module + address newDisputeModuleImpl = address( + new DisputeModule(address(newAccessManager), address(mockIpAssetRegistry), address(2)) + ); + newDisputeModule = DisputeModule( + TestProxyHelper.deployUUPSProxy( + newDisputeModuleImpl, + abi.encodeCall(DisputeModule.initialize, address(newAccessManager)) + ) + ); + + // deploy arbitration policy UMA + address newArbitrationPolicyUMAImpl = address(new ArbitrationPolicyUMA(address(newDisputeModule), newOOV3)); + newArbitrationPolicyUMA = ArbitrationPolicyUMA( + TestProxyHelper.deployUUPSProxy( + newArbitrationPolicyUMAImpl, + abi.encodeCall(ArbitrationPolicyUMA.initialize, address(newAccessManager)) + ) + ); + + // setup UMA parameters + newArbitrationPolicyUMA.setLiveness(30 days, 365 days, 66_666_666); + newArbitrationPolicyUMA.setMaxBond(susd, 25000e18); // 25k USD max bond + + // whitelist dispute tag, arbitration policy and arbitration relayer + newDisputeModule.whitelistDisputeTag("PLAGIARISM", true); + newDisputeModule.whitelistArbitrationPolicy(address(newArbitrationPolicyUMA), true); + newDisputeModule.whitelistArbitrationRelayer( + address(newArbitrationPolicyUMA), + address(newArbitrationPolicyUMA), + true + ); + newDisputeModule.setBaseArbitrationPolicy(address(newArbitrationPolicyUMA)); + + vm.label(newOOV3, "newOOV3"); + vm.label(mockAncillary, "mockAncillary"); + vm.label(susd, "susd"); + vm.label(address(newArbitrationPolicyUMA), "newArbitrationPolicyUMA"); + vm.label(address(newDisputeModule), "newDisputeModule"); + } + + function test_ArbitrationPolicyUMA_constructor_revert_ZeroDisputeModule() public { + vm.expectRevert(Errors.ArbitrationPolicyUMA__ZeroDisputeModule.selector); + new ArbitrationPolicyUMA(address(0), address(1)); + } + + function test_ArbitrationPolicyUMA_constructor_revert_ZeroOOV3() public { + vm.expectRevert(Errors.ArbitrationPolicyUMA__ZeroOOV3.selector); + new ArbitrationPolicyUMA(address(1), address(0)); + } + + function test_ArbitrationPolicyUMA_setLiveness_revert_ZeroMinLiveness() public { + vm.expectRevert(Errors.ArbitrationPolicyUMA__ZeroMinLiveness.selector); + newArbitrationPolicyUMA.setLiveness(0, 10, 10); + } + + function test_ArbitrationPolicyUMA_setLiveness_revert_ZeroMaxLiveness() public { + vm.expectRevert(Errors.ArbitrationPolicyUMA__ZeroMaxLiveness.selector); + newArbitrationPolicyUMA.setLiveness(10, 0, 10); + } + + function test_ArbitrationPolicyUMA_setLiveness_revert_MinLivenessAboveMax() public { + vm.expectRevert(Errors.ArbitrationPolicyUMA__MinLivenessAboveMax.selector); + newArbitrationPolicyUMA.setLiveness(100, 10, 10); + } + + function test_ArbitrationPolicyUMA_setLiveness_revert_IpOwnerTimePercentAboveMax() public { + vm.expectRevert(Errors.ArbitrationPolicyUMA__IpOwnerTimePercentAboveMax.selector); + newArbitrationPolicyUMA.setLiveness(10, 100, 100_000_001); + } + + function test_ArbitrationPolicyUMA_setLiveness() public { + vm.expectEmit(true, true, true, true); + emit LivenessSet(10, 100, 10); + + newArbitrationPolicyUMA.setLiveness(10, 100, 10); + + assertEq(newArbitrationPolicyUMA.minLiveness(), 10); + assertEq(newArbitrationPolicyUMA.maxLiveness(), 100); + assertEq(newArbitrationPolicyUMA.ipOwnerTimePercent(), 10); + } + + function test_ArbitrationPolicyUMA_onRaiseDispute_revert_LivenessBelowMin() public { + bytes memory claim = "test claim"; + uint64 liveness = 1; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + vm.expectRevert(Errors.ArbitrationPolicyUMA__LivenessBelowMin.selector); + newDisputeModule.raiseDispute(address(1), disputeEvidenceHashExample, "PLAGIARISM", data); + } + + function test_ArbitrationPolicyUMA_onRaiseDispute_revert_LivenessAboveMax() public { + bytes memory claim = "test claim"; + uint64 liveness = 365 days + 1; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + vm.expectRevert(Errors.ArbitrationPolicyUMA__LivenessAboveMax.selector); + newDisputeModule.raiseDispute(address(1), disputeEvidenceHashExample, "PLAGIARISM", data); + } + + function test_ArbitrationPolicyUMA_setMaxBond() public { + vm.expectEmit(true, true, true, true); + emit MaxBondSet(susd, 1); + + newArbitrationPolicyUMA.setMaxBond(susd, 1); + + assertEq(newArbitrationPolicyUMA.maxBonds(susd), 1); + } + + function test_ArbitrationPolicyUMA_onRaiseDispute_revert_NotDisputeModule() public { + vm.expectRevert(Errors.ArbitrationPolicyUMA__NotDisputeModule.selector); + newArbitrationPolicyUMA.onRaiseDispute(address(1), bytes("")); + } + + function test_ArbitrationPolicyUMA_onRaiseDispute_revert_BondAboveMax() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 25000e18 + 1; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + vm.expectRevert(Errors.ArbitrationPolicyUMA__BondAboveMax.selector); + newDisputeModule.raiseDispute(address(1), disputeEvidenceHashExample, "PLAGIARISM", data); + } + + function test_ArbitrationPolicyUMA_onRaiseDispute_revert_UnsupportedCurrency() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(address(new MockERC20())); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + vm.expectRevert("Unsupported currency"); + newDisputeModule.raiseDispute(address(1), disputeEvidenceHashExample, "PLAGIARISM", data); + } + + function test_ArbitrationPolicyUMA_onRaiseDispute_revert_UnsupportedIdentifier() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("RANDOM_IDENTIFIER"); + + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + vm.expectRevert("Unsupported identifier"); + newDisputeModule.raiseDispute(address(1), disputeEvidenceHashExample, "PLAGIARISM", data); + } + + function test_ArbitrationPolicyUMA_onRaiseDispute() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + vm.expectEmit(true, true, true, true); + emit DisputeRaisedUMA(1, address(2), claim, liveness, address(currency), bond, identifier); + + vm.startPrank(address(2)); + newDisputeModule.raiseDispute(address(1), disputeEvidenceHashExample, "PLAGIARISM", data); + + uint256 disputeId = newDisputeModule.disputeCounter(); + bytes32 assertionId = newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId); + + assertFalse(newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId) == bytes32(0)); + assertEq(newArbitrationPolicyUMA.assertionIdToDisputeId(assertionId), disputeId); + } + + function test_ArbitrationPolicyUMA_onRaiseDispute_WithBond() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 1000; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + /* vm.expectEmit(true, true, true, true); + emit DisputeRaisedUMA(1, address(2), claim, liveness, address(currency), bond, identifier); */ + + vm.startPrank(address(2)); + MockERC20(susd).mint(address(2), bond); + currency.approve(address(newArbitrationPolicyUMA), bond); + + uint256 raiserBalBefore = currency.balanceOf(address(2)); + uint256 oov3BalBefore = currency.balanceOf(address(newOOV3)); + + newDisputeModule.raiseDispute(address(1), disputeEvidenceHashExample, "PLAGIARISM", data); + + uint256 raiserBalAfter = currency.balanceOf(address(2)); + uint256 oov3BalAfter = currency.balanceOf(address(newOOV3)); + + assertEq(raiserBalBefore - raiserBalAfter, bond); + assertEq(oov3BalAfter - oov3BalBefore, bond); + + uint256 disputeId = newDisputeModule.disputeCounter(); + bytes32 assertionId = newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId); + + assertFalse(newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId) == bytes32(0)); + assertEq(newArbitrationPolicyUMA.assertionIdToDisputeId(assertionId), disputeId); + } + + function test_ArbitrationPolicyUMA_onDisputeCancel_revert_CannotCancel() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + newDisputeModule.raiseDispute(address(1), disputeEvidenceHashExample, "PLAGIARISM", data); + + vm.expectRevert(Errors.ArbitrationPolicyUMA__CannotCancel.selector); + newDisputeModule.cancelDispute(1, ""); + } + + function test_ArbitrationPolicyUMA_onDisputeJudgement_revert_AssertionNotExpired() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + uint256 disputeId = newDisputeModule.raiseDispute(address(1), disputeEvidenceHashExample, "PLAGIARISM", data); + + // settle the assertion + bytes32 assertionId = newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId); + + vm.expectRevert("Assertion not expired"); + IOOV3(newOOV3).settleAssertion(assertionId); + } + + function test_ArbitrationPolicyUMA_onDisputeJudgement_AssertionWithoutDispute() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + uint256 disputeId = newDisputeModule.raiseDispute(address(1), disputeEvidenceHashExample, "PLAGIARISM", data); + + // wait for assertion to expire + vm.warp(block.timestamp + liveness + 1); + + (, , , , , , bytes32 currentTagBefore, ) = newDisputeModule.disputes(disputeId); + + // settle the assertion + bytes32 assertionId = newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId); + IOOV3(newOOV3).settleAssertion(assertionId); + + (, , , , , , bytes32 currentTagAfter, ) = newDisputeModule.disputes(disputeId); + + assertEq(currentTagBefore, bytes32("IN_DISPUTE")); + assertEq(currentTagAfter, bytes32("PLAGIARISM")); + } + + function test_ArbitrationPolicyUMA_onDisputeJudgement_AssertionWithoutDisputeWithBond() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 1000; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + address disputer = address(2); + + vm.startPrank(disputer); + MockERC20(susd).mint(disputer, bond); + currency.approve(address(newArbitrationPolicyUMA), bond); + uint256 disputeId = newDisputeModule.raiseDispute(address(1), disputeEvidenceHashExample, "PLAGIARISM", data); + + // wait for assertion to expire + vm.warp(block.timestamp + liveness + 1); + + (, , , , , , bytes32 currentTagBefore, ) = newDisputeModule.disputes(disputeId); + + uint256 disputerBalBefore = currency.balanceOf(disputer); + + // settle the assertion + bytes32 assertionId = newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId); + IOOV3(newOOV3).settleAssertion(assertionId); + + uint256 disputerBalAfter = currency.balanceOf(disputer); + + (, , , , , , bytes32 currentTagAfter, ) = newDisputeModule.disputes(disputeId); + + assertEq(currentTagBefore, bytes32("IN_DISPUTE")); + assertEq(currentTagAfter, bytes32("PLAGIARISM")); + assertEq(disputerBalAfter - disputerBalBefore, bond); + } + + function test_ArbitrationPolicyUMA_onDisputeJudgement_AssertionWithDispute() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + address targetIpId = address(1); + + uint256 disputeId = newDisputeModule.raiseDispute(targetIpId, disputeEvidenceHashExample, "PLAGIARISM", data); + + // dispute the assertion + vm.startPrank(targetIpId); + bytes32 assertionId = newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId); + bytes32 counterEvidenceHash = bytes32("COUNTER_EVIDENCE_HASH"); + newArbitrationPolicyUMA.disputeAssertion(assertionId, counterEvidenceHash); + + (, , , , , , bytes32 currentTagBefore, ) = newDisputeModule.disputes(disputeId); + + // settle the assertion + IOOV3 oov3 = IOOV3(newOOV3); + IOOV3.Assertion memory assertion = oov3.getAssertion(assertionId); + uint64 assertionTimestamp = assertion.assertionTime; + bytes memory ancillaryData = AuxiliaryOOV3Interface(newOOV3).stampAssertion(assertionId); + IMockAncillary(mockAncillary).requestPrice(identifier, assertionTimestamp, ancillaryData); + IMockAncillary(mockAncillary).pushPrice(identifier, assertionTimestamp, ancillaryData, 1e18); + oov3.settleAssertion(assertionId); + + (, , , , , , bytes32 currentTagAfter, ) = newDisputeModule.disputes(disputeId); + + assertEq(currentTagBefore, bytes32("IN_DISPUTE")); + assertEq(currentTagAfter, bytes32("PLAGIARISM")); + } + + function test_ArbitrationPolicyUMA_disputeAssertion_revert_CannotDisputeAssertionTwice() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + address targetIpId = address(1); + uint256 disputeId = newDisputeModule.raiseDispute(targetIpId, disputeEvidenceHashExample, "PLAGIARISM", data); + + // dispute the assertion + vm.startPrank(targetIpId); + bytes32 assertionId = newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId); + bytes32 counterEvidenceHash = bytes32("COUNTER_EVIDENCE_HASH"); + newArbitrationPolicyUMA.disputeAssertion(assertionId, counterEvidenceHash); + + vm.expectRevert("Assertion already disputed"); + newArbitrationPolicyUMA.disputeAssertion(assertionId, counterEvidenceHash); + } + + function test_ArbitrationPolicyUMA_disputeAssertion_revert_NoCounterEvidence() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + address targetIpId = address(1); + uint256 disputeId = newDisputeModule.raiseDispute(targetIpId, disputeEvidenceHashExample, "PLAGIARISM", data); + + // dispute the assertion + vm.startPrank(targetIpId); + bytes32 assertionId = newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId); + + vm.expectRevert(Errors.ArbitrationPolicyUMA__NoCounterEvidence.selector); + newArbitrationPolicyUMA.disputeAssertion(assertionId, bytes32(0)); + } + + function test_ArbitrationPolicyUMA_disputeAssertion_revert_DisputeNotFound() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + address targetIpId = address(1); + uint256 disputeId = newDisputeModule.raiseDispute(targetIpId, disputeEvidenceHashExample, "PLAGIARISM", data); + + vm.expectRevert(Errors.ArbitrationPolicyUMA__DisputeNotFound.selector); + newArbitrationPolicyUMA.disputeAssertion(bytes32(0), bytes32("COUNTER_EVIDENCE_HASH")); + } + + function test_ArbitrationPolicyUMA_disputeAssertion_revert_OnlyTargetIpIdCanDispute() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + address targetIpId = address(1); + uint256 disputeId = newDisputeModule.raiseDispute(targetIpId, disputeEvidenceHashExample, "PLAGIARISM", data); + + bytes32 assertionId = newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId); + + vm.startPrank(address(2)); + vm.expectRevert( + abi.encodeWithSelector( + Errors.ArbitrationPolicyUMA__OnlyTargetIpIdCanDisputeWithinTimeWindow.selector, + 0, + liveness, + address(2) + ) + ); + newArbitrationPolicyUMA.disputeAssertion(assertionId, bytes32("COUNTER_EVIDENCE_HASH")); + } + + function test_ArbitrationPolicyUMA_disputeAssertion_IPA() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + address targetIpId = address(1); + uint256 disputeId = newDisputeModule.raiseDispute(targetIpId, disputeEvidenceHashExample, "PLAGIARISM", data); + + // dispute the assertion + vm.startPrank(targetIpId); + bytes32 assertionId = newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId); + bytes32 counterEvidenceHash = bytes32("COUNTER_EVIDENCE_HASH"); + + vm.expectEmit(true, true, true, true); + emit AssertionDisputed(assertionId, counterEvidenceHash); + + newArbitrationPolicyUMA.disputeAssertion(assertionId, counterEvidenceHash); + + (, , , , , , bytes32 currentTagBefore, ) = newDisputeModule.disputes(disputeId); + + // settle the assertion + IOOV3 oov3 = IOOV3(newOOV3); + IOOV3.Assertion memory assertion = oov3.getAssertion(assertionId); + uint64 assertionTimestamp = assertion.assertionTime; + bytes memory ancillaryData = AuxiliaryOOV3Interface(newOOV3).stampAssertion(assertionId); + IMockAncillary(mockAncillary).requestPrice(identifier, assertionTimestamp, ancillaryData); + IMockAncillary(mockAncillary).pushPrice(identifier, assertionTimestamp, ancillaryData, 0); + oov3.settleAssertion(assertionId); + + (, , , , , , bytes32 currentTagAfter, ) = newDisputeModule.disputes(disputeId); + + assertEq(currentTagBefore, bytes32("IN_DISPUTE")); + assertEq(currentTagAfter, bytes32(0)); + } + + function test_ArbitrationPolicyUMA_disputeAssertion_NotIPA() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 0; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + address targetIpId = address(1); + uint256 disputeId = newDisputeModule.raiseDispute(targetIpId, disputeEvidenceHashExample, "PLAGIARISM", data); + + vm.warp(block.timestamp + (liveness * 66_666_666) / 100_000_000 + 1); + + // dispute the assertion + vm.startPrank(address(2)); + bytes32 assertionId = newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId); + bytes32 counterEvidenceHash = bytes32("COUNTER_EVIDENCE_HASH"); + + vm.expectEmit(true, true, true, true); + emit AssertionDisputed(assertionId, counterEvidenceHash); + + newArbitrationPolicyUMA.disputeAssertion(assertionId, counterEvidenceHash); + + (, , , , , , bytes32 currentTagBefore, ) = newDisputeModule.disputes(disputeId); + + // settle the assertion + IOOV3 oov3 = IOOV3(newOOV3); + IOOV3.Assertion memory assertion = oov3.getAssertion(assertionId); + uint64 assertionTimestamp = assertion.assertionTime; + bytes memory ancillaryData = AuxiliaryOOV3Interface(newOOV3).stampAssertion(assertionId); + IMockAncillary(mockAncillary).requestPrice(identifier, assertionTimestamp, ancillaryData); + IMockAncillary(mockAncillary).pushPrice(identifier, assertionTimestamp, ancillaryData, 0); + oov3.settleAssertion(assertionId); + + (, , , , , , bytes32 currentTagAfter, ) = newDisputeModule.disputes(disputeId); + + assertEq(currentTagBefore, bytes32("IN_DISPUTE")); + assertEq(currentTagAfter, bytes32(0)); + } + + function test_ArbitrationPolicyUMA_disputeAssertion_WithBondAndIpTagged() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 1000; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + //address defenderIpIdOwner = address(1); + //address disputeInitiator = address(2); + + // raise dispute + vm.startPrank(address(2)); + MockERC20(susd).mint(address(2), bond); + currency.approve(address(newArbitrationPolicyUMA), bond); + uint256 disputeId = newDisputeModule.raiseDispute(address(1), disputeEvidenceHashExample, "PLAGIARISM", data); + vm.stopPrank(); + + // dispute the assertion + vm.startPrank(address(1)); + MockERC20(susd).mint(address(1), bond); + currency.approve(address(newArbitrationPolicyUMA), bond); + + bytes32 assertionId = newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId); + + vm.expectEmit(true, true, true, true); + emit AssertionDisputed(assertionId, bytes32("COUNTER_EVIDENCE_HASH")); + + newArbitrationPolicyUMA.disputeAssertion(assertionId, bytes32("COUNTER_EVIDENCE_HASH")); + + // settle the assertion + IOOV3 oov3 = IOOV3(newOOV3); + IOOV3.Assertion memory assertion = oov3.getAssertion(assertionId); + uint64 assertionTimestamp = assertion.assertionTime; + bytes memory ancillaryData = AuxiliaryOOV3Interface(newOOV3).stampAssertion(assertionId); + IMockAncillary(mockAncillary).requestPrice(identifier, assertionTimestamp, ancillaryData); + IMockAncillary(mockAncillary).pushPrice(identifier, assertionTimestamp, ancillaryData, 1e18); + + (, , , , , , bytes32 currentTagBefore, ) = newDisputeModule.disputes(disputeId); + + uint256 disputeInitiatorBalBefore = currency.balanceOf(address(2)); + uint256 defenderIpIdOwnerBalBefore = currency.balanceOf(address(1)); + + oov3.settleAssertion(assertionId); + + (, , , , , , bytes32 currentTagAfter, ) = newDisputeModule.disputes(disputeId); + + uint256 disputeInitiatorBalAfter = currency.balanceOf(address(2)); + uint256 defenderIpIdOwnerBalAfter = currency.balanceOf(address(1)); + + uint256 oracleFee = (oov3.burnedBondPercentage() * assertion.bond) / 1e18; + uint256 bondRecipientAmount = assertion.bond * 2 - oracleFee; + + assertEq(currentTagBefore, bytes32("IN_DISPUTE")); + assertEq(currentTagAfter, bytes32("PLAGIARISM")); + assertEq(disputeInitiatorBalAfter - disputeInitiatorBalBefore, bondRecipientAmount); + assertEq(defenderIpIdOwnerBalAfter - defenderIpIdOwnerBalBefore, 0); + } + + function test_ArbitrationPolicyUMA_disputeAssertion_WithBondAndIpNotTagged() public { + bytes memory claim = "test claim"; + uint64 liveness = 3600 * 24 * 30; + IERC20 currency = IERC20(susd); + uint256 bond = 1000; + bytes32 identifier = bytes32("ASSERT_TRUTH"); + bytes memory data = abi.encode(claim, liveness, currency, bond, identifier); + + //address defenderIpIdOwner = address(1); + //address disputeInitiator = address(2); + + // raise dispute + vm.startPrank(address(2)); + MockERC20(susd).mint(address(2), bond); + currency.approve(address(newArbitrationPolicyUMA), bond); + uint256 disputeId = newDisputeModule.raiseDispute(address(1), disputeEvidenceHashExample, "PLAGIARISM", data); + vm.stopPrank(); + + // dispute the assertion + vm.startPrank(address(1)); + MockERC20(susd).mint(address(1), bond); + currency.approve(address(newArbitrationPolicyUMA), bond); + + bytes32 assertionId = newArbitrationPolicyUMA.disputeIdToAssertionId(disputeId); + + vm.expectEmit(true, true, true, true); + emit AssertionDisputed(assertionId, bytes32("COUNTER_EVIDENCE_HASH")); + + newArbitrationPolicyUMA.disputeAssertion(assertionId, bytes32("COUNTER_EVIDENCE_HASH")); + + // settle the assertion + IOOV3 oov3 = IOOV3(newOOV3); + IOOV3.Assertion memory assertion = oov3.getAssertion(assertionId); + uint64 assertionTimestamp = assertion.assertionTime; + bytes memory ancillaryData = AuxiliaryOOV3Interface(newOOV3).stampAssertion(assertionId); + IMockAncillary(mockAncillary).requestPrice(identifier, assertionTimestamp, ancillaryData); + IMockAncillary(mockAncillary).pushPrice(identifier, assertionTimestamp, ancillaryData, 0); + + (, , , , , , bytes32 currentTagBefore, ) = newDisputeModule.disputes(disputeId); + + uint256 disputeInitiatorBalBefore = currency.balanceOf(address(2)); + uint256 defenderIpIdOwnerBalBefore = currency.balanceOf(address(1)); + + oov3.settleAssertion(assertionId); + + (, , , , , , bytes32 currentTagAfter, ) = newDisputeModule.disputes(disputeId); + + uint256 disputeInitiatorBalAfter = currency.balanceOf(address(2)); + uint256 defenderIpIdOwnerBalAfter = currency.balanceOf(address(1)); + + uint256 oracleFee = (oov3.burnedBondPercentage() * assertion.bond) / 1e18; + uint256 bondRecipientAmount = assertion.bond * 2 - oracleFee; + + assertEq(currentTagBefore, bytes32("IN_DISPUTE")); + assertEq(currentTagAfter, bytes32(0)); + assertEq(disputeInitiatorBalAfter - disputeInitiatorBalBefore, 0); + assertEq(defenderIpIdOwnerBalAfter - defenderIpIdOwnerBalBefore, bondRecipientAmount); + } + + function test_ArbitrationPolicyUMA_assertionResolvedCallback_revert_NotOOV3() public { + vm.expectRevert(Errors.ArbitrationPolicyUMA__NotOOV3.selector); + newArbitrationPolicyUMA.assertionResolvedCallback(bytes32(0), false); + } + + function test_ArbitrationPolicyUMA_assertionDisputedCallback_revert_NotOOV3() public { + vm.expectRevert(Errors.ArbitrationPolicyUMA__NotOOV3.selector); + newArbitrationPolicyUMA.assertionDisputedCallback(bytes32(0)); + } + + function test_ArbitrationPolicyUMA_assertionDisputedCallback_revert_NoCounterEvidence() public { + vm.startPrank(newOOV3); + + vm.expectRevert(Errors.ArbitrationPolicyUMA__NoCounterEvidence.selector); + newArbitrationPolicyUMA.assertionDisputedCallback(bytes32(0)); + } +} + +interface AuxiliaryOOV3Interface { + function stampAssertion(bytes32 assertionId) external view returns (bytes memory); +}