diff --git a/docs/src/content/modules/resolution/private_erc20_resolution_module.md b/docs/src/content/modules/resolution/private_erc20_resolution_module.md index c73c0dd6..d644343e 100644 --- a/docs/src/content/modules/resolution/private_erc20_resolution_module.md +++ b/docs/src/content/modules/resolution/private_erc20_resolution_module.md @@ -10,7 +10,7 @@ The `PrivateERC20ResolutionModule` is a contract that allows users to vote on a ### Key methods -- `decodeRequestData(bytes32 _requestId)`: Returns the decoded data for a request. +- `decodeRequestData(bytes calldata _data)`: Returns the decoded data for a request. - `startResolution(bytes32 _disputeId)`: Starts the committing phase for a dispute. - `commitVote(bytes32 _requestId, bytes32 _disputeId, bytes32 _commitment)`: Stores a commitment for a vote cast by a voter. - `revealVote(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes, bytes32 _salt)`: Reveals a vote cast by a voter. diff --git a/solidity/contracts/modules/resolution/PrivateERC20ResolutionModule.sol b/solidity/contracts/modules/resolution/PrivateERC20ResolutionModule.sol index 079326fd..5cebd018 100644 --- a/solidity/contracts/modules/resolution/PrivateERC20ResolutionModule.sol +++ b/solidity/contracts/modules/resolution/PrivateERC20ResolutionModule.sol @@ -1,143 +1,162 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// // solhint-disable-next-line no-unused-import -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; -// import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; - -// // solhint-disable-next-line no-unused-import -// import {Module, IModule} from '@defi-wonderland/prophet-core-contracts/solidity/contracts/Module.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; - -// import {IPrivateERC20ResolutionModule} from '../../../interfaces/modules/resolution/IPrivateERC20ResolutionModule.sol'; - -// contract PrivateERC20ResolutionModule is Module, IPrivateERC20ResolutionModule { -// using SafeERC20 for IERC20; -// using EnumerableSet for EnumerableSet.AddressSet; - -// /// @inheritdoc IPrivateERC20ResolutionModule -// mapping(bytes32 _disputeId => Escalation _escalation) public escalations; -// /** -// * @notice The data of the voters for a given dispute -// */ -// mapping(bytes32 _disputeId => mapping(address _voter => VoterData)) internal _votersData; -// /** -// * @notice The voters addresses for a given dispute -// */ -// mapping(bytes32 _disputeId => EnumerableSet.AddressSet _votersSet) internal _voters; - -// constructor(IOracle _oracle) Module(_oracle) {} - -// /// @inheritdoc IModule -// function moduleName() external pure returns (string memory _moduleName) { -// return 'PrivateERC20ResolutionModule'; -// } - -// /// @inheritdoc IPrivateERC20ResolutionModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } - -// /// @inheritdoc IPrivateERC20ResolutionModule -// function startResolution(bytes32 _disputeId) external onlyOracle { -// escalations[_disputeId].startTime = block.timestamp; -// emit CommittingPhaseStarted(block.timestamp, _disputeId); -// } - -// /// @inheritdoc IPrivateERC20ResolutionModule -// function commitVote(bytes32 _requestId, bytes32 _disputeId, bytes32 _commitment) public { -// IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId); -// if (_dispute.createdAt == 0) revert PrivateERC20ResolutionModule_NonExistentDispute(); -// if (_dispute.status != IOracle.DisputeStatus.None) revert PrivateERC20ResolutionModule_AlreadyResolved(); - -// uint256 _startTime = escalations[_disputeId].startTime; -// if (_startTime == 0) revert PrivateERC20ResolutionModule_DisputeNotEscalated(); - -// RequestParameters memory _params = decodeRequestData(_requestId); -// uint256 _committingDeadline = _startTime + _params.committingTimeWindow; -// if (block.timestamp >= _committingDeadline) revert PrivateERC20ResolutionModule_CommittingPhaseOver(); - -// if (_commitment == bytes32('')) revert PrivateERC20ResolutionModule_EmptyCommitment(); -// _votersData[_disputeId][msg.sender] = VoterData({numOfVotes: 0, commitment: _commitment}); - -// emit VoteCommitted(msg.sender, _disputeId, _commitment); -// } - -// /// @inheritdoc IPrivateERC20ResolutionModule -// function revealVote(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes, bytes32 _salt) public { -// Escalation memory _escalation = escalations[_disputeId]; -// if (_escalation.startTime == 0) revert PrivateERC20ResolutionModule_DisputeNotEscalated(); - -// RequestParameters memory _params = decodeRequestData(_requestId); -// (uint256 _revealStartTime, uint256 _revealEndTime) = ( -// _escalation.startTime + _params.committingTimeWindow, -// _escalation.startTime + _params.committingTimeWindow + _params.revealingTimeWindow -// ); -// if (block.timestamp <= _revealStartTime) revert PrivateERC20ResolutionModule_OnGoingCommittingPhase(); -// if (block.timestamp > _revealEndTime) revert PrivateERC20ResolutionModule_RevealingPhaseOver(); - -// VoterData storage _voterData = _votersData[_disputeId][msg.sender]; - -// if (_voterData.commitment != keccak256(abi.encode(msg.sender, _disputeId, _numberOfVotes, _salt))) { -// revert PrivateERC20ResolutionModule_WrongRevealData(); -// } - -// _voterData.numOfVotes = _numberOfVotes; -// _voterData.commitment = bytes32(''); -// _voters[_disputeId].add(msg.sender); -// escalations[_disputeId].totalVotes += _numberOfVotes; - -// _params.votingToken.safeTransferFrom(msg.sender, address(this), _numberOfVotes); - -// emit VoteRevealed(msg.sender, _disputeId, _numberOfVotes); -// } - -// /// @inheritdoc IPrivateERC20ResolutionModule -// function resolveDispute(bytes32 _disputeId) external onlyOracle { -// IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId); -// if (_dispute.createdAt == 0) revert PrivateERC20ResolutionModule_NonExistentDispute(); -// if (_dispute.status != IOracle.DisputeStatus.None) revert PrivateERC20ResolutionModule_AlreadyResolved(); - -// Escalation memory _escalation = escalations[_disputeId]; -// if (_escalation.startTime == 0) revert PrivateERC20ResolutionModule_DisputeNotEscalated(); - -// RequestParameters memory _params = decodeRequestData(_dispute.requestId); - -// if (block.timestamp < _escalation.startTime + _params.committingTimeWindow) { -// revert PrivateERC20ResolutionModule_OnGoingCommittingPhase(); -// } -// if (block.timestamp < _escalation.startTime + _params.committingTimeWindow + _params.revealingTimeWindow) { -// revert PrivateERC20ResolutionModule_OnGoingRevealingPhase(); -// } - -// uint256 _quorumReached = _escalation.totalVotes >= _params.minVotesForQuorum ? 1 : 0; - -// address[] memory __voters = _voters[_disputeId].values(); - -// if (_quorumReached == 1) { -// ORACLE.updateDisputeStatus(_disputeId, IOracle.DisputeStatus.Won); -// emit DisputeResolved(_dispute.requestId, _disputeId, IOracle.DisputeStatus.Won); -// } else { -// ORACLE.updateDisputeStatus(_disputeId, IOracle.DisputeStatus.Lost); -// emit DisputeResolved(_dispute.requestId, _disputeId, IOracle.DisputeStatus.Lost); -// } - -// uint256 _length = __voters.length; -// for (uint256 _i; _i < _length;) { -// _params.votingToken.safeTransfer(__voters[_i], _votersData[_disputeId][__voters[_i]].numOfVotes); -// unchecked { -// ++_i; -// } -// } -// } - -// /// @inheritdoc IPrivateERC20ResolutionModule -// function computeCommitment( -// bytes32 _disputeId, -// uint256 _numberOfVotes, -// bytes32 _salt -// ) external view returns (bytes32 _commitment) { -// _commitment = keccak256(abi.encode(msg.sender, _disputeId, _numberOfVotes, _salt)); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// solhint-disable-next-line no-unused-import +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; +import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; + +// solhint-disable-next-line no-unused-import +import {Module, IModule} from '@defi-wonderland/prophet-core-contracts/solidity/contracts/Module.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; + +import {IPrivateERC20ResolutionModule} from '../../../interfaces/modules/resolution/IPrivateERC20ResolutionModule.sol'; + +contract PrivateERC20ResolutionModule is Module, IPrivateERC20ResolutionModule { + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; + + /// @inheritdoc IPrivateERC20ResolutionModule + mapping(bytes32 _disputeId => Escalation _escalation) public escalations; + /** + * @notice The data of the voters for a given dispute + */ + mapping(bytes32 _disputeId => mapping(address _voter => VoterData)) internal _votersData; + /** + * @notice The voters addresses for a given dispute + */ + mapping(bytes32 _disputeId => EnumerableSet.AddressSet _votersSet) internal _voters; + + constructor(IOracle _oracle) Module(_oracle) {} + + /// @inheritdoc IModule + function moduleName() external pure returns (string memory _moduleName) { + return 'PrivateERC20ResolutionModule'; + } + + /// @inheritdoc IPrivateERC20ResolutionModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } + + /// @inheritdoc IPrivateERC20ResolutionModule + function startResolution( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + escalations[_disputeId].startTime = block.timestamp; + emit CommittingPhaseStarted(block.timestamp, _disputeId); + } + + /// @inheritdoc IPrivateERC20ResolutionModule + function commitVote(IOracle.Request calldata _request, IOracle.Dispute calldata _dispute, bytes32 _commitment) public { + bytes32 _disputeId = _getId(_dispute); + if (ORACLE.createdAt(_disputeId) == 0) revert PrivateERC20ResolutionModule_NonExistentDispute(); + if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.None) { + revert PrivateERC20ResolutionModule_AlreadyResolved(); + } + + uint256 _startTime = escalations[_disputeId].startTime; + if (_startTime == 0) revert PrivateERC20ResolutionModule_DisputeNotEscalated(); + + RequestParameters memory _params = decodeRequestData(_request.resolutionModuleData); + uint256 _committingDeadline = _startTime + _params.committingTimeWindow; + if (block.timestamp >= _committingDeadline) revert PrivateERC20ResolutionModule_CommittingPhaseOver(); + + if (_commitment == bytes32('')) revert PrivateERC20ResolutionModule_EmptyCommitment(); + _votersData[_disputeId][msg.sender] = VoterData({numOfVotes: 0, commitment: _commitment}); + + emit VoteCommitted(msg.sender, _disputeId, _commitment); + } + + /// @inheritdoc IPrivateERC20ResolutionModule + function revealVote( + IOracle.Request calldata _request, + IOracle.Dispute calldata _dispute, + uint256 _numberOfVotes, + bytes32 _salt + ) public { + bytes32 _disputeId = _getId(_dispute); + Escalation memory _escalation = escalations[_disputeId]; + if (_escalation.startTime == 0) revert PrivateERC20ResolutionModule_DisputeNotEscalated(); + + RequestParameters memory _params = decodeRequestData(_request.resolutionModuleData); + (uint256 _revealStartTime, uint256 _revealEndTime) = ( + _escalation.startTime + _params.committingTimeWindow, + _escalation.startTime + _params.committingTimeWindow + _params.revealingTimeWindow + ); + if (block.timestamp <= _revealStartTime) revert PrivateERC20ResolutionModule_OnGoingCommittingPhase(); + if (block.timestamp > _revealEndTime) revert PrivateERC20ResolutionModule_RevealingPhaseOver(); + + VoterData storage _voterData = _votersData[_disputeId][msg.sender]; + + if (_voterData.commitment != keccak256(abi.encode(msg.sender, _disputeId, _numberOfVotes, _salt))) { + revert PrivateERC20ResolutionModule_WrongRevealData(); + } + + _voterData.numOfVotes = _numberOfVotes; + _voterData.commitment = bytes32(''); + _voters[_disputeId].add(msg.sender); + escalations[_disputeId].totalVotes += _numberOfVotes; + + _params.votingToken.safeTransferFrom(msg.sender, address(this), _numberOfVotes); + + emit VoteRevealed(msg.sender, _disputeId, _numberOfVotes); + } + + /// @inheritdoc IPrivateERC20ResolutionModule + function resolveDispute( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + if (ORACLE.createdAt(_disputeId) == 0) revert PrivateERC20ResolutionModule_NonExistentDispute(); + if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.None) { + revert PrivateERC20ResolutionModule_AlreadyResolved(); + } + + Escalation memory _escalation = escalations[_disputeId]; + if (_escalation.startTime == 0) revert PrivateERC20ResolutionModule_DisputeNotEscalated(); + + RequestParameters memory _params = decodeRequestData(_request.resolutionModuleData); + + if (block.timestamp < _escalation.startTime + _params.committingTimeWindow) { + revert PrivateERC20ResolutionModule_OnGoingCommittingPhase(); + } + if (block.timestamp < _escalation.startTime + _params.committingTimeWindow + _params.revealingTimeWindow) { + revert PrivateERC20ResolutionModule_OnGoingRevealingPhase(); + } + + uint256 _quorumReached = _escalation.totalVotes >= _params.minVotesForQuorum ? 1 : 0; + + address[] memory __voters = _voters[_disputeId].values(); + + if (_quorumReached == 1) { + ORACLE.updateDisputeStatus(_request, _response, _dispute, IOracle.DisputeStatus.Won); + emit DisputeResolved(_dispute.requestId, _disputeId, IOracle.DisputeStatus.Won); + } else { + ORACLE.updateDisputeStatus(_request, _response, _dispute, IOracle.DisputeStatus.Lost); + emit DisputeResolved(_dispute.requestId, _disputeId, IOracle.DisputeStatus.Lost); + } + + uint256 _length = __voters.length; + for (uint256 _i; _i < _length;) { + _params.votingToken.safeTransfer(__voters[_i], _votersData[_disputeId][__voters[_i]].numOfVotes); + unchecked { + ++_i; + } + } + } + + /// @inheritdoc IPrivateERC20ResolutionModule + function computeCommitment( + bytes32 _disputeId, + uint256 _numberOfVotes, + bytes32 _salt + ) external view returns (bytes32 _commitment) { + _commitment = keccak256(abi.encode(msg.sender, _disputeId, _numberOfVotes, _salt)); + } +} diff --git a/solidity/interfaces/modules/resolution/IPrivateERC20ResolutionModule.sol b/solidity/interfaces/modules/resolution/IPrivateERC20ResolutionModule.sol index 66b1249b..eaea62e1 100644 --- a/solidity/interfaces/modules/resolution/IPrivateERC20ResolutionModule.sol +++ b/solidity/interfaces/modules/resolution/IPrivateERC20ResolutionModule.sol @@ -1,200 +1,216 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// import {IResolutionModule} from -// '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/resolution/IResolutionModule.sol'; -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {IAccountingExtension} from '../../extensions/IAccountingExtension.sol'; - -// /* -// * @title PrivateERC20ResolutionModule -// * @notice Module allowing users to vote on a dispute using ERC20 -// * tokens through a commit/reveal pattern. -// */ -// interface IPrivateERC20ResolutionModule is IResolutionModule { -// /*/////////////////////////////////////////////////////////////// -// EVENTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice A commitment has been provided by a voter -// * @param _voter The user who provided a commitment of a vote -// * @param _disputeId The id of the dispute being voted on -// * @param _commitment The commitment provided by the voter -// */ -// event VoteCommitted(address _voter, bytes32 _disputeId, bytes32 _commitment); - -// /** -// * @notice A vote has been revealed by a voter providing -// * the salt used to compute the commitment -// * @param _voter The user who revealed his vote -// * @param _disputeId The id of the dispute being voted on -// * @param _numberOfVotes The number of votes cast -// */ -// event VoteRevealed(address _voter, bytes32 _disputeId, uint256 _numberOfVotes); - -// /** -// * @notice The phase of committing votes has started -// * @param _startTime The timestamp at which the phase started -// * @param _disputeId The id of the dispute being voted on -// */ -// event CommittingPhaseStarted(uint256 _startTime, bytes32 _disputeId); - -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Thrown when the dispute has not been escalated -// */ -// error PrivateERC20ResolutionModule_DisputeNotEscalated(); - -// /** -// * @notice Thrown when trying to commit a vote after the committing deadline -// */ -// error PrivateERC20ResolutionModule_CommittingPhaseOver(); - -// /** -// * @notice Thrown when trying to reveal a vote after the revealing deadline -// */ -// error PrivateERC20ResolutionModule_RevealingPhaseOver(); - -// /** -// * @notice Thrown when trying to resolve a dispute during the committing phase -// */ -// error PrivateERC20ResolutionModule_OnGoingCommittingPhase(); - -// /** -// * @notice Thrown when trying to resolve a dispute during the revealing phase -// */ -// error PrivateERC20ResolutionModule_OnGoingRevealingPhase(); - -// /** -// * @notice Thrown when trying to resolve a dispute that does not exist -// */ -// error PrivateERC20ResolutionModule_NonExistentDispute(); - -// /** -// * @notice Thrown when trying to commit an empty commitment -// */ -// error PrivateERC20ResolutionModule_EmptyCommitment(); - -// /** -// * @notice Thrown when trying to reveal a vote with data that does not match the stored commitment -// */ -// error PrivateERC20ResolutionModule_WrongRevealData(); - -// /** -// * @notice Thrown when trying to resolve a dispute that is already resolved -// */ -// error PrivateERC20ResolutionModule_AlreadyResolved(); - -// /*/////////////////////////////////////////////////////////////// -// STRUCTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Parameters of the request as stored in the module -// * @param accountingExtension The accounting extension used to bond and release tokens -// * @param token The token used to vote -// * @param minVotesForQuorum The minimum amount of votes to win the dispute -// * @param committingTimeWindow The amount of time to commit votes from the escalation of the dispute -// * @param revealingTimeWindow The amount of time to reveal votes from the committing phase -// */ -// struct RequestParameters { -// IAccountingExtension accountingExtension; -// IERC20 votingToken; -// uint256 minVotesForQuorum; -// uint256 committingTimeWindow; -// uint256 revealingTimeWindow; -// } - -// /** -// * @notice Escalation data for a dispute -// * @param startTime The timestamp at which the dispute was escalated -// * @param totalVotes The total amount of votes cast for the dispute -// */ - -// struct Escalation { -// uint256 startTime; -// uint256 totalVotes; -// } - -// /** -// * @notice Voting data for each voter -// * @param numOfVotes The amount of votes cast for the dispute -// * @param commitment The commitment provided by the voter -// */ -// struct VoterData { -// uint256 numOfVotes; -// bytes32 commitment; -// } - -// /*/////////////////////////////////////////////////////////////// -// VARIABLES -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Returns the escalation data for a dispute -// * @param _disputeId The id of the dispute -// * @return _startTime The timestamp at which the dispute was escalated -// * @return _totalVotes The total amount of votes cast for the dispute -// */ -// function escalations(bytes32 _disputeId) external view returns (uint256 _startTime, uint256 _totalVotes); - -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Starts the committing phase for a dispute -// * @dev Only callable by the Oracle -// * @param _disputeId The id of the dispute to start resolution of -// */ -// function startResolution(bytes32 _disputeId) external; - -// /** -// * @notice Stores a commitment for a vote cast by a voter -// * @dev Committing multiple times and overwriting a previous commitment is allowed -// * @param _requestId The id of the request being disputed -// * @param _disputeId The id of the dispute being voted on -// * @param _commitment The commitment computed from the provided data and the user's address -// */ -// function commitVote(bytes32 _requestId, bytes32 _disputeId, bytes32 _commitment) external; - -// /** -// * @notice Reveals a vote cast by a voter -// * @dev The user must have previously approved the module to transfer the tokens -// * @param _requestId The id of the request being disputed -// * @param _disputeId The id of the dispute being voted on -// * @param _numberOfVotes The amount of votes being revealed -// * @param _salt The salt used to compute the commitment -// */ -// function revealVote(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes, bytes32 _salt) external; - -// /** -// * @notice Resolves a dispute by tallying the votes and executing the winning outcome -// * @dev Only callable by the Oracle -// * @param _disputeId The id of the dispute being resolved -// */ -// function resolveDispute(bytes32 _disputeId) external; - -// /** -// * @notice Returns the decoded data for a request -// * @param _requestId The ID of the request -// * @return _params The struct containing the parameters for the request -// */ -// function decodeRequestData(bytes32 _requestId) external view returns (RequestParameters memory _params); - -// /** -// * @notice Computes a valid commitment for the revealing phase -// * @param _disputeId The id of the dispute being voted on -// * @param _numberOfVotes The amount of votes being cast -// * @return _commitment The commitment computed from the provided data and the user's address -// */ -// function computeCommitment( -// bytes32 _disputeId, -// uint256 _numberOfVotes, -// bytes32 _salt -// ) external view returns (bytes32 _commitment); -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IResolutionModule} from + '@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/resolution/IResolutionModule.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IAccountingExtension} from '../../extensions/IAccountingExtension.sol'; + +/* + * @title PrivateERC20ResolutionModule + * @notice Module allowing users to vote on a dispute using ERC20 + * tokens through a commit/reveal pattern. + */ +interface IPrivateERC20ResolutionModule is IResolutionModule { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice A commitment has been provided by a voter + * @param _voter The user who provided a commitment of a vote + * @param _disputeId The id of the dispute being voted on + * @param _commitment The commitment provided by the voter + */ + event VoteCommitted(address _voter, bytes32 _disputeId, bytes32 _commitment); + + /** + * @notice A vote has been revealed by a voter providing + * the salt used to compute the commitment + * @param _voter The user who revealed his vote + * @param _disputeId The id of the dispute being voted on + * @param _numberOfVotes The number of votes cast + */ + event VoteRevealed(address _voter, bytes32 _disputeId, uint256 _numberOfVotes); + + /** + * @notice The phase of committing votes has started + * @param _startTime The timestamp at which the phase started + * @param _disputeId The id of the dispute being voted on + */ + event CommittingPhaseStarted(uint256 _startTime, bytes32 _disputeId); + + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Thrown when the dispute has not been escalated + */ + error PrivateERC20ResolutionModule_DisputeNotEscalated(); + + /** + * @notice Thrown when trying to commit a vote after the committing deadline + */ + error PrivateERC20ResolutionModule_CommittingPhaseOver(); + + /** + * @notice Thrown when trying to reveal a vote after the revealing deadline + */ + error PrivateERC20ResolutionModule_RevealingPhaseOver(); + + /** + * @notice Thrown when trying to resolve a dispute during the committing phase + */ + error PrivateERC20ResolutionModule_OnGoingCommittingPhase(); + + /** + * @notice Thrown when trying to resolve a dispute during the revealing phase + */ + error PrivateERC20ResolutionModule_OnGoingRevealingPhase(); + + /** + * @notice Thrown when trying to resolve a dispute that does not exist + */ + error PrivateERC20ResolutionModule_NonExistentDispute(); + + /** + * @notice Thrown when trying to commit an empty commitment + */ + error PrivateERC20ResolutionModule_EmptyCommitment(); + + /** + * @notice Thrown when trying to reveal a vote with data that does not match the stored commitment + */ + error PrivateERC20ResolutionModule_WrongRevealData(); + + /** + * @notice Thrown when trying to resolve a dispute that is already resolved + */ + error PrivateERC20ResolutionModule_AlreadyResolved(); + + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Parameters of the request as stored in the module + * @param accountingExtension The accounting extension used to bond and release tokens + * @param token The token used to vote + * @param minVotesForQuorum The minimum amount of votes to win the dispute + * @param committingTimeWindow The amount of time to commit votes from the escalation of the dispute + * @param revealingTimeWindow The amount of time to reveal votes from the committing phase + */ + struct RequestParameters { + IAccountingExtension accountingExtension; + IERC20 votingToken; + uint256 minVotesForQuorum; + uint256 committingTimeWindow; + uint256 revealingTimeWindow; + } + + /** + * @notice Escalation data for a dispute + * @param startTime The timestamp at which the dispute was escalated + * @param totalVotes The total amount of votes cast for the dispute + */ + + struct Escalation { + uint256 startTime; + uint256 totalVotes; + } + + /** + * @notice Voting data for each voter + * @param numOfVotes The amount of votes cast for the dispute + * @param commitment The commitment provided by the voter + */ + struct VoterData { + uint256 numOfVotes; + bytes32 commitment; + } + + /*/////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the escalation data for a dispute + * @param _disputeId The id of the dispute + * @return _startTime The timestamp at which the dispute was escalated + * @return _totalVotes The total amount of votes cast for the dispute + */ + function escalations(bytes32 _disputeId) external view returns (uint256 _startTime, uint256 _totalVotes); + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Starts the committing phase for a dispute + * @dev Only callable by the Oracle + * @param _disputeId The id of the dispute to start resolution of + */ + function startResolution( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external; + + /** + * @notice Stores a commitment for a vote cast by a voter + * @dev Committing multiple times and overwriting a previous commitment is allowed + * @param _commitment The commitment computed from the provided data and the user's address + */ + function commitVote( + IOracle.Request calldata _request, + IOracle.Dispute calldata _dispute, + bytes32 _commitment + ) external; + + /** + * @notice Reveals a vote cast by a voter + * @dev The user must have previously approved the module to transfer the tokens + * @param _numberOfVotes The amount of votes being revealed + * @param _salt The salt used to compute the commitment + */ + function revealVote( + IOracle.Request calldata _request, + IOracle.Dispute calldata _dispute, + uint256 _numberOfVotes, + bytes32 _salt + ) external; + + /** + * @notice Resolves a dispute by tallying the votes and executing the winning outcome + * @dev Only callable by the Oracle + * @param _disputeId The id of the dispute being resolved + */ + function resolveDispute( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external; + + /** + * @notice Returns the decoded data for a request + * @param _data The encoded request parameters + * @return _params The struct containing the parameters for the request + */ + function decodeRequestData(bytes calldata _data) external view returns (RequestParameters memory _params); + + /** + * @notice Computes a valid commitment for the revealing phase + * @param _disputeId The id of the dispute being voted on + * @param _numberOfVotes The amount of votes being cast + * @return _commitment The commitment computed from the provided data and the user's address + */ + function computeCommitment( + bytes32 _disputeId, + uint256 _numberOfVotes, + bytes32 _salt + ) external view returns (bytes32 _commitment); +} diff --git a/solidity/test/unit/modules/resolution/PrivateERC20ResolutionModule.t.sol b/solidity/test/unit/modules/resolution/PrivateERC20ResolutionModule.t.sol index 769a9d86..d3437b31 100644 --- a/solidity/test/unit/modules/resolution/PrivateERC20ResolutionModule.t.sol +++ b/solidity/test/unit/modules/resolution/PrivateERC20ResolutionModule.t.sol @@ -1,595 +1,570 @@ -// // SPDX-License-Identifier: AGPL-3.0-only -// pragma solidity ^0.8.19; - -// import 'forge-std/Test.sol'; - -// import {Helpers} from '../../../utils/Helpers.sol'; - -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; -// import {IModule} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IModule.sol'; - -// import { -// PrivateERC20ResolutionModule, -// IPrivateERC20ResolutionModule -// } from '../../../../contracts/modules/resolution/PrivateERC20ResolutionModule.sol'; -// import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; - -// contract ForTest_PrivateERC20ResolutionModule is PrivateERC20ResolutionModule { -// constructor(IOracle _oracle) PrivateERC20ResolutionModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } - -// function forTest_setEscalation( -// bytes32 _disputeId, -// PrivateERC20ResolutionModule.Escalation calldata __escalation -// ) public { -// escalations[_disputeId] = __escalation; -// } - -// function forTest_setVoterData( -// bytes32 _disputeId, -// address _voter, -// IPrivateERC20ResolutionModule.VoterData memory _data -// ) public { -// _votersData[_disputeId][_voter] = _data; -// } - -// function forTest_getVoterData( -// bytes32 _disputeId, -// address _voter -// ) public view returns (IPrivateERC20ResolutionModule.VoterData memory _data) { -// _data = _votersData[_disputeId][_voter]; -// } -// } - -// contract BaseTest is Test, Helpers { -// // The target contract -// ForTest_PrivateERC20ResolutionModule public module; -// // A mock oracle -// IOracle public oracle; -// // A mock accounting extension -// IAccountingExtension public accounting; -// // A mock token -// IERC20 public token; -// // Mock EOA proposer -// address public proposer = makeAddr('proposer'); -// // Mock EOA disputer -// address public disputer = makeAddr('disputer'); - -// // Mocking module events -// event CommittingPhaseStarted(uint256 _startTime, bytes32 _disputeId); -// event VoteCommitted(address _voter, bytes32 _disputeId, bytes32 _commitment); -// event VoteRevealed(address _voter, bytes32 _disputeId, uint256 _numberOfVotes); -// event DisputeResolved(bytes32 indexed _requestId, bytes32 indexed _disputeId, IOracle.DisputeStatus _status); - -// /** -// * @notice Deploy the target and mock oracle+accounting extension -// */ -// function setUp() public { -// oracle = IOracle(makeAddr('Oracle')); -// vm.etch(address(oracle), hex'069420'); - -// accounting = IAccountingExtension(makeAddr('AccountingExtension')); -// vm.etch(address(accounting), hex'069420'); - -// token = IERC20(makeAddr('ERC20')); -// vm.etch(address(token), hex'069420'); - -// proposer = makeAddr('proposer'); -// disputer = makeAddr('disputer'); - -// module = new ForTest_PrivateERC20ResolutionModule(oracle); -// } - -// /** -// * @dev Helper function to store commitments and reveal votes. -// */ -// function _populateVoters( -// bytes32 _requestId, -// bytes32 _disputeId, -// uint256 _amountOfVoters, -// uint256 _amountOfVotes -// ) internal returns (uint256 _totalVotesCast) { -// for (uint256 _i = 1; _i <= _amountOfVoters;) { -// vm.warp(120_000); -// vm.startPrank(vm.addr(_i)); -// bytes32 _commitment = module.computeCommitment(_disputeId, _amountOfVotes, bytes32(_i)); // index as salt -// module.commitVote(_requestId, _disputeId, _commitment); -// vm.warp(140_001); -// vm.mockCall( -// address(token), -// abi.encodeCall(IERC20.transferFrom, (vm.addr(_i), address(module), _amountOfVotes)), -// abi.encode() -// ); -// module.revealVote(_requestId, _disputeId, _amountOfVotes, bytes32(_i)); -// vm.stopPrank(); -// _totalVotesCast += _amountOfVotes; -// unchecked { -// ++_i; -// } -// } -// } -// } - -// contract PrivateERC20ResolutionModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleName() public { -// assertEq(module.moduleName(), 'PrivateERC20ResolutionModule'); -// } -// } - -// contract PrivateERC20ResolutionModule_Unit_StartResolution is BaseTest { -// /** -// * @notice Test that the startResolution is correctly called and the committing phase is started -// */ -// function test_startResolution(bytes32 _disputeId) public { -// module.forTest_setEscalation(_disputeId, IPrivateERC20ResolutionModule.Escalation({startTime: 0, totalVotes: 0})); - -// // Check: does revert if called by address != oracle? -// vm.expectRevert(IModule.Module_OnlyOracle.selector); -// module.startResolution(_disputeId); - -// // Check: emits CommittingPhaseStarted event? -// vm.expectEmit(true, true, true, true); -// emit CommittingPhaseStarted(block.timestamp, _disputeId); - -// vm.prank(address(oracle)); -// module.startResolution(_disputeId); - -// (uint256 _startTime,) = module.escalations(_disputeId); - -// // Check: startTime is set to block.timestamp? -// assertEq(_startTime, block.timestamp); -// } -// } - -// contract PrivateERC20ResolutionModule_Unit_CommitVote is BaseTest { -// /** -// * @notice Test that a user can store a vote commitment for a dispute -// */ -// function test_commitVote( -// bytes32 _requestId, -// bytes32 _disputeId, -// uint256 _amountOfVotes, -// bytes32 _salt, -// address _voter -// ) public { -// // Mock the dispute -// IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); - -// // Store mock escalation data with startTime 100_000 -// module.forTest_setEscalation( -// _disputeId, -// IPrivateERC20ResolutionModule.Escalation({ -// startTime: 100_000, -// totalVotes: 0 // Initial amount of votes -// }) -// ); - -// // Store mock request data with 40_000 committing time window -// uint256 _minVotesForQuorum = 1; -// uint256 _committingTimeWindow = 40_000; -// uint256 _revealingTimeWindow = 40_000; - -// module.forTest_setRequestData( -// _requestId, -// abi.encode(address(accounting), token, _minVotesForQuorum, _committingTimeWindow, _revealingTimeWindow) -// ); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// // Set timestamp for valid committingTimeWindow -// vm.warp(123_456); - -// // Compute commitment -// vm.startPrank(_voter); -// bytes32 _commitment = module.computeCommitment(_disputeId, _amountOfVotes, _salt); - -// // Check: is event emitted? -// vm.expectEmit(true, true, true, true); -// emit VoteCommitted(_voter, _disputeId, _commitment); - -// // Check: does it revert if no commitment is given? -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_EmptyCommitment.selector); -// module.commitVote(_requestId, _disputeId, bytes32('')); - -// // Compute and store commitment -// module.commitVote(_requestId, _disputeId, _commitment); - -// // Check: reverts if empty commitment is given? -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_EmptyCommitment.selector); -// module.commitVote(_requestId, _disputeId, bytes32('')); - -// // Check: is the commitment stored? -// IPrivateERC20ResolutionModule.VoterData memory _voterData = module.forTest_getVoterData(_disputeId, _voter); -// assertEq(_voterData.commitment, _commitment); - -// bytes32 _newCommitment = module.computeCommitment(_disputeId, uint256(_salt), bytes32(_amountOfVotes)); -// module.commitVote(_requestId, _disputeId, _newCommitment); -// vm.stopPrank(); - -// // Check: is voters data updated with new commitment? -// IPrivateERC20ResolutionModule.VoterData memory _newVoterData = module.forTest_getVoterData(_disputeId, _voter); -// assertEq(_newVoterData.commitment, _newCommitment); -// } - -// /** -// * @notice Test that `commitVote` reverts if there is no dispute with the given`_disputeId` -// */ -// function test_revertIfNonExistentDispute(bytes32 _requestId, bytes32 _disputeId, bytes32 _commitment) public { -// IOracle.Dispute memory _mockDispute = IOracle.Dispute({ -// disputer: address(0), -// responseId: bytes32(0), -// proposer: address(0), -// requestId: bytes32(0), -// status: IOracle.DisputeStatus.None, -// createdAt: 0 -// }); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// // Check: does it revert if no dispute exists? -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_NonExistentDispute.selector); -// module.commitVote(_requestId, _disputeId, _commitment); -// } - -// /** -// * @notice Test that `commitVote` reverts if called with `_disputeId` of an already resolved dispute. -// */ -// function test_revertIfAlreadyResolved(bytes32 _requestId, bytes32 _disputeId, bytes32 _commitment) public { -// // Mock dispute already resolved => DisputeStatus.Lost -// IOracle.Dispute memory _mockDispute = IOracle.Dispute({ -// disputer: disputer, -// responseId: bytes32('response'), -// proposer: proposer, -// requestId: _requestId, -// status: IOracle.DisputeStatus.Lost, -// createdAt: block.timestamp -// }); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// // Check: does it revert if the dispute is already resolved? -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_AlreadyResolved.selector); -// module.commitVote(_requestId, _disputeId, _commitment); -// } - -// /** -// * @notice Test that `commitVote` reverts if called with `_disputeId` of a non-escalated dispute. -// */ -// function test_revertIfNotEscalated(bytes32 _requestId, bytes32 _disputeId, bytes32 _commitment) public { -// // Mock the oracle response for looking up a dispute -// IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// // Check: reverts if dispute is not escalated? == no escalation data -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_DisputeNotEscalated.selector); -// module.commitVote(_requestId, _disputeId, _commitment); -// } - -// /** -// * @notice Test that `commitVote` reverts if called outside of the committing time window. -// */ -// function test_revertIfCommittingPhaseOver(bytes32 _requestId, bytes32 _disputeId, bytes32 _commitment) public { -// // Mock the oracle response for looking up a dispute -// IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// module.forTest_setEscalation( -// _disputeId, -// IPrivateERC20ResolutionModule.Escalation({ -// startTime: 100_000, -// totalVotes: 0 // Initial amount of votes -// }) -// ); - -// uint256 _minVotesForQuorum = 1; -// uint256 _committingTimeWindow = 40_000; -// uint256 _revealingTimeWindow = 40_000; - -// module.forTest_setRequestData( -// _requestId, -// abi.encode(address(accounting), token, _minVotesForQuorum, _committingTimeWindow, _revealingTimeWindow) -// ); - -// // Warp to invalid timestamp for commitment -// vm.warp(150_000); - -// // Check: does it revert if the committing phase is over? -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_CommittingPhaseOver.selector); -// module.commitVote(_requestId, _disputeId, _commitment); -// } -// } - -// contract PrivateERC20ResolutionModule_Unit_RevealVote is BaseTest { -// /** -// * @notice Test revealing votes with proper timestamp, dispute status and commitment data. -// */ -// function test_revealVote( -// bytes32 _requestId, -// bytes32 _disputeId, -// uint256 _amountOfVotes, -// bytes32 _salt, -// address _voter -// ) public { -// // Store mock escalation data with startTime 100_000 -// module.forTest_setEscalation( -// _disputeId, -// IPrivateERC20ResolutionModule.Escalation({ -// startTime: 100_000, -// totalVotes: 0 // Initial amount of votes -// }) -// ); - -// // Store mock request data with 40_000 committing time window -// module.forTest_setRequestData( -// _requestId, abi.encode(address(accounting), token, uint256(1), uint256(40_000), uint256(40_000)) -// ); - -// // Store commitment -// vm.prank(_voter); -// bytes32 _commitment = module.computeCommitment(_disputeId, _amountOfVotes, _salt); -// module.forTest_setVoterData( -// _disputeId, _voter, IPrivateERC20ResolutionModule.VoterData({numOfVotes: 0, commitment: _commitment}) -// ); - -// // Mock and expect IERC20.transferFrom to be called -// _mockAndExpect( -// address(token), abi.encodeCall(IERC20.transferFrom, (_voter, address(module), _amountOfVotes)), abi.encode() -// ); - -// // Warp to revealing phase -// vm.warp(150_000); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true); -// emit VoteRevealed(_voter, _disputeId, _amountOfVotes); - -// vm.prank(_voter); -// module.revealVote(_requestId, _disputeId, _amountOfVotes, _salt); - -// (, uint256 _totalVotes) = module.escalations(_disputeId); -// // Check: is totalVotes updated? -// assertEq(_totalVotes, _amountOfVotes); - -// // Check: is voter data proplerly updated? -// IPrivateERC20ResolutionModule.VoterData memory _voterData = module.forTest_getVoterData(_disputeId, _voter); -// assertEq(_voterData.numOfVotes, _amountOfVotes); -// } - -// /** -// * @notice Test that `revealVote` reverts if called with `_disputeId` of a non-escalated dispute. -// */ -// function test_revertIfNotEscalated( -// bytes32 _requestId, -// bytes32 _disputeId, -// uint256 _numberOfVotes, -// bytes32 _salt -// ) public { -// // Check: does it revert if the dispute is not escalated? -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_DisputeNotEscalated.selector); -// module.revealVote(_requestId, _disputeId, _numberOfVotes, _salt); -// } - -// /** -// * @notice Test that `revealVote` reverts if called outside the revealing time window. -// */ -// function test_revertIfInvalidPhase( -// bytes32 _requestId, -// bytes32 _disputeId, -// uint256 _numberOfVotes, -// bytes32 _salt, -// uint256 _timestamp -// ) public { -// vm.assume(_timestamp >= 100_000 && (_timestamp <= 140_000 || _timestamp > 180_000)); - -// module.forTest_setEscalation( -// _disputeId, -// IPrivateERC20ResolutionModule.Escalation({ -// startTime: 100_000, -// totalVotes: 0 // Initial amount of votes -// }) -// ); - -// // Store request data -// uint256 _minVotesForQuorum = 1; -// uint256 _committingTimeWindow = 40_000; -// uint256 _revealingTimeWindow = 40_000; - -// module.forTest_setRequestData( -// _requestId, -// abi.encode(address(accounting), token, _minVotesForQuorum, _committingTimeWindow, _revealingTimeWindow) -// ); - -// // Jump to timestamp -// vm.warp(_timestamp); - -// if (_timestamp <= 140_000) { -// // Check: does it revert if trying to reveal during the committing phase? -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_OnGoingCommittingPhase.selector); -// module.revealVote(_requestId, _disputeId, _numberOfVotes, _salt); -// } else { -// // Check: does it revert if trying to reveal after the revealing phase? -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_RevealingPhaseOver.selector); -// module.revealVote(_requestId, _disputeId, _numberOfVotes, _salt); -// } -// } - -// /** -// * @notice Test that `revealVote` reverts if called with revealing parameters (`_disputeId`, `_numberOfVotes`, `_salt`) -// * that do not compute to the stored commitment. -// */ -// function test_revertIfFalseCommitment( -// bytes32 _requestId, -// bytes32 _disputeId, -// uint256 _amountOfVotes, -// uint256 _wrongAmountOfVotes, -// bytes32 _salt, -// bytes32 _wrongSalt, -// address _voter, -// address _wrongVoter -// ) public { -// vm.assume(_amountOfVotes != _wrongAmountOfVotes); -// vm.assume(_salt != _wrongSalt); -// vm.assume(_voter != _wrongVoter); - -// module.forTest_setEscalation( -// _disputeId, -// IPrivateERC20ResolutionModule.Escalation({ -// startTime: 100_000, -// totalVotes: 0 // Initial amount of votes -// }) -// ); - -// // Store request data -// uint256 _minVotesForQuorum = 1; -// uint256 _committingTimeWindow = 40_000; -// uint256 _revealingTimeWindow = 40_000; - -// module.forTest_setRequestData( -// _requestId, -// abi.encode(address(accounting), token, _minVotesForQuorum, _committingTimeWindow, _revealingTimeWindow) -// ); -// vm.warp(150_000); - -// vm.startPrank(_voter); -// bytes32 _commitment = module.computeCommitment(_disputeId, _amountOfVotes, _salt); -// module.forTest_setVoterData( -// _disputeId, _voter, IPrivateERC20ResolutionModule.VoterData({numOfVotes: 0, commitment: _commitment}) -// ); - -// // Check: does it revert if the commitment is not valid? (wrong salt) -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_WrongRevealData.selector); -// module.revealVote(_requestId, _disputeId, _amountOfVotes, _wrongSalt); - -// // Check: does it revert if the commitment is not valid? (wrong amount of votes) -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_WrongRevealData.selector); -// module.revealVote(_requestId, _disputeId, _wrongAmountOfVotes, _salt); - -// vm.stopPrank(); - -// // Check: does it revert if the commitment is not valid? (wrong voter) -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_WrongRevealData.selector); -// vm.prank(_wrongVoter); -// module.revealVote(_requestId, _disputeId, _amountOfVotes, _salt); -// } -// } - -// contract PrivateERC20ResolutionModule_Unit_ResolveDispute is BaseTest { -// /** -// * @notice Test that a dispute is resolved, the tokens are transferred back to the voters and the dispute status updated. -// */ -// function test_resolveDispute(bytes32 _requestId, bytes32 _disputeId, uint16 _minVotesForQuorum) public { -// // Store mock dispute and mock calls -// IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// // Store request data -// uint256 _committingTimeWindow = 40_000; -// uint256 _revealingTimeWindow = 40_000; - -// module.forTest_setRequestData( -// _requestId, -// abi.encode(address(accounting), token, _minVotesForQuorum, _committingTimeWindow, _revealingTimeWindow) -// ); - -// // Store escalation data with startTime 100_000 and votes 0 -// module.forTest_setEscalation( -// _disputeId, IPrivateERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0}) -// ); - -// uint256 _votersAmount = 5; - -// // Make 5 addresses cast 100 votes each -// uint256 _totalVotesCast = _populateVoters(_requestId, _disputeId, _votersAmount, 100); - -// // Warp to resolving phase -// vm.warp(190_000); - -// // Mock and expect token transfers (should happen always) -// for (uint256 _i = 1; _i <= _votersAmount;) { -// _mockAndExpect(address(token), abi.encodeCall(IERC20.transfer, (vm.addr(_i), 100)), abi.encode()); -// unchecked { -// ++_i; -// } -// } - -// // If quorum reached, check for dispute status update and event emission -// IOracle.DisputeStatus _newStatus = -// _totalVotesCast >= _minVotesForQuorum ? IOracle.DisputeStatus.Won : IOracle.DisputeStatus.Lost; - -// // Mock and expect IOracle.updateDisputeStatus to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.updateDisputeStatus, (_disputeId, _newStatus)), abi.encode()); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true); -// emit DisputeResolved(_requestId, _disputeId, _newStatus); - -// // Check: does it revert if called by address != oracle? -// vm.expectRevert(IModule.Module_OnlyOracle.selector); -// module.resolveDispute(_disputeId); - -// vm.prank(address(oracle)); -// module.resolveDispute(_disputeId); -// } - -// /** -// * @notice Test that `resolveDispute` reverts if called during committing or revealing time window. -// */ -// function test_revertIfWrongPhase(bytes32 _requestId, bytes32 _disputeId, uint256 _timestamp) public { -// _timestamp = bound(_timestamp, 1, 1_000_000); - -// // Store mock dispute and mock calls -// IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); - -// // Mock and expect IOracle.getDispute to be called -// _mockAndExpect(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// module.forTest_setEscalation( -// _disputeId, -// IPrivateERC20ResolutionModule.Escalation({ -// startTime: 1, -// totalVotes: 0 // Initial amount of votes -// }) -// ); - -// // Store request data -// uint256 _minVotesForQuorum = 1; -// uint256 _committingTimeWindow = 500_000; -// uint256 _revealingTimeWindow = 1_000_000; - -// module.forTest_setRequestData( -// _requestId, -// abi.encode(address(accounting), token, _minVotesForQuorum, _committingTimeWindow, _revealingTimeWindow) -// ); - -// // Jump to timestamp -// vm.warp(_timestamp); - -// if (_timestamp <= 500_000) { -// // Check: does it revert if trying to resolve during the committing phase? -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_OnGoingCommittingPhase.selector); -// vm.prank(address(oracle)); -// module.resolveDispute(_disputeId); -// } else { -// // Check: does it revert if trying to resolve during the revealing phase? -// vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_OnGoingRevealingPhase.selector); -// vm.prank(address(oracle)); -// module.resolveDispute(_disputeId); -// } -// } -// } +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import 'forge-std/Test.sol'; + +import {Helpers} from '../../../utils/Helpers.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; +import {IModule} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IModule.sol'; + +import { + PrivateERC20ResolutionModule, + IPrivateERC20ResolutionModule +} from '../../../../contracts/modules/resolution/PrivateERC20ResolutionModule.sol'; +import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol'; + +contract ForTest_PrivateERC20ResolutionModule is PrivateERC20ResolutionModule { + constructor(IOracle _oracle) PrivateERC20ResolutionModule(_oracle) {} + + function forTest_setEscalation( + bytes32 _disputeId, + PrivateERC20ResolutionModule.Escalation calldata __escalation + ) public { + escalations[_disputeId] = __escalation; + } + + function forTest_setVoterData( + bytes32 _disputeId, + address _voter, + IPrivateERC20ResolutionModule.VoterData memory _data + ) public { + _votersData[_disputeId][_voter] = _data; + } + + function forTest_getVoterData( + bytes32 _disputeId, + address _voter + ) public view returns (IPrivateERC20ResolutionModule.VoterData memory _data) { + _data = _votersData[_disputeId][_voter]; + } +} + +contract BaseTest is Test, Helpers { + // The target contract + ForTest_PrivateERC20ResolutionModule public module; + // A mock oracle + IOracle public oracle; + // A mock accounting extension + IAccountingExtension public accounting; + // A mock token + IERC20 public token; + // Mock EOA proposer + address public proposer = makeAddr('proposer'); + // Mock EOA disputer + address public disputer = makeAddr('disputer'); + // Create a new dummy dispute + IOracle.Dispute public mockDispute; + // Create a new dummy response + IOracle.Response public mockResponse; + bytes32 public mockId = bytes32('69'); + + // Mocking module events + event CommittingPhaseStarted(uint256 _startTime, bytes32 _disputeId); + event VoteCommitted(address _voter, bytes32 _disputeId, bytes32 _commitment); + event VoteRevealed(address _voter, bytes32 _disputeId, uint256 _numberOfVotes); + event DisputeResolved(bytes32 indexed _requestId, bytes32 indexed _disputeId, IOracle.DisputeStatus _status); + + /** + * @notice Deploy the target and mock oracle+accounting extension + */ + function setUp() public { + oracle = IOracle(makeAddr('Oracle')); + vm.etch(address(oracle), hex'069420'); + + accounting = IAccountingExtension(makeAddr('AccountingExtension')); + vm.etch(address(accounting), hex'069420'); + + token = IERC20(makeAddr('ERC20')); + vm.etch(address(token), hex'069420'); + + proposer = makeAddr('proposer'); + disputer = makeAddr('disputer'); + + module = new ForTest_PrivateERC20ResolutionModule(oracle); + + mockDispute = + IOracle.Dispute({disputer: disputer, proposer: proposer, responseId: bytes32('69'), requestId: bytes32('69')}); + mockResponse = IOracle.Response({proposer: proposer, requestId: mockId, response: bytes('')}); + } + + /** + * @dev Helper function to store commitments and reveal votes. + */ + function _populateVoters( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _amountOfVoters, + uint256 _amountOfVotes, + IOracle.Request calldata _request + ) internal returns (uint256 _totalVotesCast) { + for (uint256 _i = 1; _i <= _amountOfVoters;) { + vm.warp(120_000); + vm.startPrank(vm.addr(_i)); + bytes32 _commitment = module.computeCommitment(_disputeId, _amountOfVotes, bytes32(_i)); // index as salt + module.commitVote(_request, mockDispute, _commitment); + vm.warp(140_001); + vm.mockCall( + address(token), + abi.encodeCall(IERC20.transferFrom, (vm.addr(_i), address(module), _amountOfVotes)), + abi.encode() + ); + module.revealVote(_request, mockDispute, _amountOfVotes, bytes32(_i)); + vm.stopPrank(); + _totalVotesCast += _amountOfVotes; + unchecked { + ++_i; + } + } + } +} + +contract PrivateERC20ResolutionModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleName() public { + assertEq(module.moduleName(), 'PrivateERC20ResolutionModule'); + } +} + +contract PrivateERC20ResolutionModule_Unit_StartResolution is BaseTest { + /** + * @notice Test that the startResolution is correctly called and the committing phase is started + */ + function test_startResolution(bytes32 _disputeId, IOracle.Request calldata _request) public { + module.forTest_setEscalation(_disputeId, IPrivateERC20ResolutionModule.Escalation({startTime: 0, totalVotes: 0})); + + // Check: does revert if called by address != oracle? + vm.expectRevert(IModule.Module_OnlyOracle.selector); + module.startResolution(_disputeId, _request, mockResponse, mockDispute); + + // Check: emits CommittingPhaseStarted event? + vm.expectEmit(true, true, true, true); + emit CommittingPhaseStarted(block.timestamp, _disputeId); + + vm.prank(address(oracle)); + module.startResolution(_disputeId, _request, mockResponse, mockDispute); + + (uint256 _startTime,) = module.escalations(_disputeId); + + // Check: startTime is set to block.timestamp? + assertEq(_startTime, block.timestamp); + } +} + +contract PrivateERC20ResolutionModule_Unit_CommitVote is BaseTest { + /** + * @notice Test that a user can store a vote commitment for a dispute + */ + function test_commitVote( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _amountOfVotes, + bytes32 _salt, + address _voter, + IOracle.Request calldata _request + ) public { + // Mock the dispute + IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); + + // Store mock escalation data with startTime 100_000 + module.forTest_setEscalation( + _disputeId, + IPrivateERC20ResolutionModule.Escalation({ + startTime: 100_000, + totalVotes: 0 // Initial amount of votes + }) + ); + + // Store mock request data with 40_000 committing time window + uint256 _minVotesForQuorum = 1; + uint256 _committingTimeWindow = 40_000; + uint256 _revealingTimeWindow = 40_000; + + // Set timestamp for valid committingTimeWindow + vm.warp(123_456); + + // Compute commitment + vm.startPrank(_voter); + bytes32 _commitment = module.computeCommitment(_disputeId, _amountOfVotes, _salt); + + // Check: is event emitted? + vm.expectEmit(true, true, true, true); + emit VoteCommitted(_voter, _disputeId, _commitment); + + // Check: does it revert if no commitment is given? + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_EmptyCommitment.selector); + module.commitVote(_request, mockDispute, bytes32('')); + + // Compute and store commitment + module.commitVote(_request, mockDispute, _commitment); + + // Check: reverts if empty commitment is given? + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_EmptyCommitment.selector); + module.commitVote(_request, mockDispute, bytes32('')); + + // Check: is the commitment stored? + IPrivateERC20ResolutionModule.VoterData memory _voterData = module.forTest_getVoterData(_disputeId, _voter); + assertEq(_voterData.commitment, _commitment); + + bytes32 _newCommitment = module.computeCommitment(_disputeId, uint256(_salt), bytes32(_amountOfVotes)); + module.commitVote(_request, mockDispute, _newCommitment); + vm.stopPrank(); + + // Check: is voters data updated with new commitment? + IPrivateERC20ResolutionModule.VoterData memory _newVoterData = module.forTest_getVoterData(_disputeId, _voter); + assertEq(_newVoterData.commitment, _newCommitment); + } + + /** + * @notice Test that `commitVote` reverts if there is no dispute with the given`_disputeId` + */ + function test_revertIfNonExistentDispute( + bytes32 _requestId, + bytes32 _disputeId, + bytes32 _commitment, + IOracle.Request calldata _request + ) public { + IOracle.Dispute memory _mockDispute = + IOracle.Dispute({disputer: address(0), responseId: bytes32(0), proposer: address(0), requestId: bytes32(0)}); + + // Check: does it revert if no dispute exists? + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_NonExistentDispute.selector); + module.commitVote(_request, mockDispute, _commitment); + } + + /** + * @notice Test that `commitVote` reverts if called with `_disputeId` of an already resolved dispute. + */ + function test_revertIfAlreadyResolved( + bytes32 _requestId, + bytes32 _disputeId, + bytes32 _commitment, + IOracle.Request calldata _request + ) public { + // Mock dispute already resolved => DisputeStatus.Lost + IOracle.Dispute memory _mockDispute = + IOracle.Dispute({disputer: disputer, responseId: bytes32('response'), proposer: proposer, requestId: _requestId}); + + // Check: does it revert if the dispute is already resolved? + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_AlreadyResolved.selector); + module.commitVote(_request, mockDispute, _commitment); + } + + /** + * @notice Test that `commitVote` reverts if called with `_disputeId` of a non-escalated dispute. + */ + function test_revertIfNotEscalated( + bytes32 _requestId, + bytes32 _disputeId, + bytes32 _commitment, + IOracle.Request calldata _request + ) public { + mockDispute.requestId = _requestId; + + // Check: reverts if dispute is not escalated? == no escalation data + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_DisputeNotEscalated.selector); + module.commitVote(_request, mockDispute, _commitment); + } + + /** + * @notice Test that `commitVote` reverts if called outside of the committing time window. + */ + function test_revertIfCommittingPhaseOver( + bytes32 _requestId, + bytes32 _disputeId, + bytes32 _commitment, + IOracle.Request calldata _request + ) public { + mockDispute.requestId = _requestId; + + module.forTest_setEscalation( + _disputeId, + IPrivateERC20ResolutionModule.Escalation({ + startTime: 100_000, + totalVotes: 0 // Initial amount of votes + }) + ); + + uint256 _minVotesForQuorum = 1; + uint256 _committingTimeWindow = 40_000; + uint256 _revealingTimeWindow = 40_000; + + // Warp to invalid timestamp for commitment + vm.warp(150_000); + + // Check: does it revert if the committing phase is over? + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_CommittingPhaseOver.selector); + module.commitVote(_request, mockDispute, _commitment); + } +} + +contract PrivateERC20ResolutionModule_Unit_RevealVote is BaseTest { + /** + * @notice Test revealing votes with proper timestamp, dispute status and commitment data. + */ + function test_revealVote( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _amountOfVotes, + bytes32 _salt, + address _voter, + IOracle.Request calldata _request + ) public { + // Store mock escalation data with startTime 100_000 + module.forTest_setEscalation( + _disputeId, + IPrivateERC20ResolutionModule.Escalation({ + startTime: 100_000, + totalVotes: 0 // Initial amount of votes + }) + ); + + // Store commitment + vm.prank(_voter); + bytes32 _commitment = module.computeCommitment(_disputeId, _amountOfVotes, _salt); + module.forTest_setVoterData( + _disputeId, _voter, IPrivateERC20ResolutionModule.VoterData({numOfVotes: 0, commitment: _commitment}) + ); + + // Mock and expect IERC20.transferFrom to be called + _mockAndExpect( + address(token), abi.encodeCall(IERC20.transferFrom, (_voter, address(module), _amountOfVotes)), abi.encode() + ); + + // Warp to revealing phase + vm.warp(150_000); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true); + emit VoteRevealed(_voter, _disputeId, _amountOfVotes); + + vm.prank(_voter); + module.revealVote(_request, mockDispute, _amountOfVotes, _salt); + + (, uint256 _totalVotes) = module.escalations(_disputeId); + // Check: is totalVotes updated? + assertEq(_totalVotes, _amountOfVotes); + + // Check: is voter data proplerly updated? + IPrivateERC20ResolutionModule.VoterData memory _voterData = module.forTest_getVoterData(_disputeId, _voter); + assertEq(_voterData.numOfVotes, _amountOfVotes); + } + + /** + * @notice Test that `revealVote` reverts if called with `_disputeId` of a non-escalated dispute. + */ + function test_revertIfNotEscalated( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _numberOfVotes, + bytes32 _salt, + IOracle.Request calldata _request + ) public { + // Check: does it revert if the dispute is not escalated? + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_DisputeNotEscalated.selector); + module.revealVote(_request, mockDispute, _numberOfVotes, _salt); + } + + /** + * @notice Test that `revealVote` reverts if called outside the revealing time window. + */ + function test_revertIfInvalidPhase( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _numberOfVotes, + bytes32 _salt, + uint256 _timestamp, + IOracle.Request calldata _request + ) public { + vm.assume(_timestamp >= 100_000 && (_timestamp <= 140_000 || _timestamp > 180_000)); + + module.forTest_setEscalation( + _disputeId, + IPrivateERC20ResolutionModule.Escalation({ + startTime: 100_000, + totalVotes: 0 // Initial amount of votes + }) + ); + + // Store request data + uint256 _minVotesForQuorum = 1; + uint256 _committingTimeWindow = 40_000; + uint256 _revealingTimeWindow = 40_000; + + // Jump to timestamp + vm.warp(_timestamp); + + if (_timestamp <= 140_000) { + // Check: does it revert if trying to reveal during the committing phase? + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_OnGoingCommittingPhase.selector); + module.revealVote(_request, mockDispute, _numberOfVotes, _salt); + } else { + // Check: does it revert if trying to reveal after the revealing phase? + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_RevealingPhaseOver.selector); + module.revealVote(_request, mockDispute, _numberOfVotes, _salt); + } + } + + /** + * @notice Test that `revealVote` reverts if called with revealing parameters (`_disputeId`, `_numberOfVotes`, `_salt`) + * that do not compute to the stored commitment. + */ + function test_revertIfFalseCommitment( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _amountOfVotes, + uint256 _wrongAmountOfVotes, + bytes32 _salt, + bytes32 _wrongSalt, + address _voter, + address _wrongVoter, + IOracle.Request calldata _request + ) public { + vm.assume(_amountOfVotes != _wrongAmountOfVotes); + vm.assume(_salt != _wrongSalt); + vm.assume(_voter != _wrongVoter); + + module.forTest_setEscalation( + _disputeId, + IPrivateERC20ResolutionModule.Escalation({ + startTime: 100_000, + totalVotes: 0 // Initial amount of votes + }) + ); + + // Store request data + uint256 _minVotesForQuorum = 1; + uint256 _committingTimeWindow = 40_000; + uint256 _revealingTimeWindow = 40_000; + vm.warp(150_000); + + vm.startPrank(_voter); + bytes32 _commitment = module.computeCommitment(_disputeId, _amountOfVotes, _salt); + module.forTest_setVoterData( + _disputeId, _voter, IPrivateERC20ResolutionModule.VoterData({numOfVotes: 0, commitment: _commitment}) + ); + + // Check: does it revert if the commitment is not valid? (wrong salt) + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_WrongRevealData.selector); + module.revealVote(_request, mockDispute, _amountOfVotes, _wrongSalt); + + // Check: does it revert if the commitment is not valid? (wrong amount of votes) + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_WrongRevealData.selector); + module.revealVote(_request, mockDispute, _wrongAmountOfVotes, _salt); + + vm.stopPrank(); + + // Check: does it revert if the commitment is not valid? (wrong voter) + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_WrongRevealData.selector); + vm.prank(_wrongVoter); + module.revealVote(_request, mockDispute, _amountOfVotes, _salt); + } +} + +contract PrivateERC20ResolutionModule_Unit_ResolveDispute is BaseTest { + /** + * @notice Test that a dispute is resolved, the tokens are transferred back to the voters and the dispute status updated. + */ + function test_resolveDispute( + bytes32 _requestId, + bytes32 _disputeId, + uint16 _minVotesForQuorum, + IOracle.Request calldata _request + ) public { + // Store mock dispute and mock calls + IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); + + // Store request data + uint256 _committingTimeWindow = 40_000; + uint256 _revealingTimeWindow = 40_000; + + // Store escalation data with startTime 100_000 and votes 0 + module.forTest_setEscalation( + _disputeId, IPrivateERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0}) + ); + + uint256 _votersAmount = 5; + + // Make 5 addresses cast 100 votes each + uint256 _totalVotesCast = _populateVoters(_requestId, _disputeId, _votersAmount, 100, _request); + + // Warp to resolving phase + vm.warp(190_000); + + // Mock and expect token transfers (should happen always) + for (uint256 _i = 1; _i <= _votersAmount;) { + _mockAndExpect(address(token), abi.encodeCall(IERC20.transfer, (vm.addr(_i), 100)), abi.encode()); + unchecked { + ++_i; + } + } + + // If quorum reached, check for dispute status update and event emission + IOracle.DisputeStatus _newStatus = + _totalVotesCast >= _minVotesForQuorum ? IOracle.DisputeStatus.Won : IOracle.DisputeStatus.Lost; + + // Mock and expect IOracle.updateDisputeStatus to be called + _mockAndExpect( + address(oracle), + abi.encodeCall(IOracle.updateDisputeStatus, (_request, mockResponse, mockDispute, _newStatus)), + abi.encode() + ); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true); + emit DisputeResolved(_requestId, _disputeId, _newStatus); + + // Check: does it revert if called by address != oracle? + vm.expectRevert(IModule.Module_OnlyOracle.selector); + module.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + + vm.prank(address(oracle)); + module.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + } + + /** + * @notice Test that `resolveDispute` reverts if called during committing or revealing time window. + */ + function test_revertIfWrongPhase( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _timestamp, + IOracle.Request calldata _request + ) public { + _timestamp = bound(_timestamp, 1, 1_000_000); + + // Store mock dispute and mock calls + IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId, disputer, proposer); + + module.forTest_setEscalation( + _disputeId, + IPrivateERC20ResolutionModule.Escalation({ + startTime: 1, + totalVotes: 0 // Initial amount of votes + }) + ); + + // Store request data + uint256 _minVotesForQuorum = 1; + uint256 _committingTimeWindow = 500_000; + uint256 _revealingTimeWindow = 1_000_000; + + // Jump to timestamp + vm.warp(_timestamp); + + if (_timestamp <= 500_000) { + // Check: does it revert if trying to resolve during the committing phase? + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_OnGoingCommittingPhase.selector); + vm.prank(address(oracle)); + module.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + } else { + // Check: does it revert if trying to resolve during the revealing phase? + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_OnGoingRevealingPhase.selector); + vm.prank(address(oracle)); + module.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + } + } +}