diff --git a/docs/src/content/modules/resolution/erc20_resolution_module.md b/docs/src/content/modules/resolution/erc20_resolution_module.md index e8073699..3905c8fb 100644 --- a/docs/src/content/modules/resolution/erc20_resolution_module.md +++ b/docs/src/content/modules/resolution/erc20_resolution_module.md @@ -10,7 +10,7 @@ The `ERC20ResolutionModule` is a dispute resolution module that decides on the o ### Key Methods -- `decodeRequestData(bytes32 _requestId)`: Decodes the request data associated with a given request ID. +- `decodeRequestData(bytes calldata _data)`: Decodes the request data associated with a given request ID. - `startResolution(bytes32 _disputeId)`: Starts the resolution process for a given dispute. - `castVote(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes)`: Allows a user to cast votes for a dispute. - `resolveDispute(bytes32 _disputeId)`: Resolves a dispute based on the votes cast. diff --git a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol index 78b0ce2c..dfd9197b 100644 --- a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol +++ b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol @@ -1,112 +1,125 @@ -// // 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 {IERC20ResolutionModule} from '../../../interfaces/modules/resolution/IERC20ResolutionModule.sol'; - -// contract ERC20ResolutionModule is Module, IERC20ResolutionModule { -// using SafeERC20 for IERC20; -// using EnumerableSet for EnumerableSet.AddressSet; - -// /// @inheritdoc IERC20ResolutionModule -// mapping(bytes32 _disputeId => Escalation _escalation) public escalations; - -// /// @inheritdoc IERC20ResolutionModule -// mapping(bytes32 _disputeId => mapping(address _voter => uint256 _numOfVotes)) public votes; - -// mapping(bytes32 _disputeId => EnumerableSet.AddressSet _votersSet) private _voters; - -// constructor(IOracle _oracle) Module(_oracle) {} - -// /// @inheritdoc IModule -// function moduleName() external pure returns (string memory _moduleName) { -// return 'ERC20ResolutionModule'; -// } - -// /// @inheritdoc IERC20ResolutionModule -// function decodeRequestData(bytes32 _requestId) public view returns (RequestParameters memory _params) { -// _params = abi.decode(requestData[_requestId], (RequestParameters)); -// } - -// /// @inheritdoc IERC20ResolutionModule -// function startResolution(bytes32 _disputeId) external onlyOracle { -// escalations[_disputeId].startTime = block.timestamp; -// emit VotingPhaseStarted(block.timestamp, _disputeId); -// } - -// /// @inheritdoc IERC20ResolutionModule -// function castVote(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes) public { -// IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId); -// if (_dispute.createdAt == 0) revert ERC20ResolutionModule_NonExistentDispute(); -// if (_dispute.status != IOracle.DisputeStatus.None) revert ERC20ResolutionModule_AlreadyResolved(); - -// Escalation memory _escalation = escalations[_disputeId]; -// if (_escalation.startTime == 0) revert ERC20ResolutionModule_DisputeNotEscalated(); - -// RequestParameters memory _params = decodeRequestData(_requestId); -// uint256 _deadline = _escalation.startTime + _params.timeUntilDeadline; -// if (block.timestamp >= _deadline) revert ERC20ResolutionModule_VotingPhaseOver(); - -// votes[_disputeId][msg.sender] += _numberOfVotes; - -// _voters[_disputeId].add(msg.sender); -// escalations[_disputeId].totalVotes += _numberOfVotes; - -// _params.votingToken.safeTransferFrom(msg.sender, address(this), _numberOfVotes); -// emit VoteCast(msg.sender, _disputeId, _numberOfVotes); -// } - -// /// @inheritdoc IERC20ResolutionModule -// function resolveDispute(bytes32 _disputeId) external onlyOracle { -// // 0. Check disputeId actually exists and that it isn't resolved already -// IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId); -// if (_dispute.createdAt == 0) revert ERC20ResolutionModule_NonExistentDispute(); -// if (_dispute.status != IOracle.DisputeStatus.None) revert ERC20ResolutionModule_AlreadyResolved(); - -// Escalation memory _escalation = escalations[_disputeId]; -// // 1. Check that the dispute is actually escalated -// if (_escalation.startTime == 0) revert ERC20ResolutionModule_DisputeNotEscalated(); - -// // 2. Check that voting deadline is over -// RequestParameters memory _params = decodeRequestData(_dispute.requestId); -// uint256 _deadline = _escalation.startTime + _params.timeUntilDeadline; -// if (block.timestamp < _deadline) revert ERC20ResolutionModule_OnGoingVotingPhase(); - -// uint256 _quorumReached = _escalation.totalVotes >= _params.minVotesForQuorum ? 1 : 0; - -// address[] memory __voters = _voters[_disputeId].values(); - -// // 5. Update status -// 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 _votersLength = __voters.length; - -// // 6. Return tokens -// for (uint256 _i; _i < _votersLength;) { -// address _voter = __voters[_i]; -// _params.votingToken.safeTransfer(_voter, votes[_disputeId][_voter]); -// unchecked { -// ++_i; -// } -// } -// } - -// /// @inheritdoc IERC20ResolutionModule -// function getVoters(bytes32 _disputeId) external view returns (address[] memory __voters) { -// __voters = _voters[_disputeId].values(); -// } -// } +// 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 {IERC20ResolutionModule} from '../../../interfaces/modules/resolution/IERC20ResolutionModule.sol'; + +contract ERC20ResolutionModule is Module, IERC20ResolutionModule { + using SafeERC20 for IERC20; + using EnumerableSet for EnumerableSet.AddressSet; + + /// @inheritdoc IERC20ResolutionModule + mapping(bytes32 _disputeId => Escalation _escalation) public escalations; + + /// @inheritdoc IERC20ResolutionModule + mapping(bytes32 _disputeId => mapping(address _voter => uint256 _numOfVotes)) public votes; + + mapping(bytes32 _disputeId => EnumerableSet.AddressSet _votersSet) private _voters; + + constructor(IOracle _oracle) Module(_oracle) {} + + /// @inheritdoc IModule + function moduleName() external pure returns (string memory _moduleName) { + return 'ERC20ResolutionModule'; + } + + /// @inheritdoc IERC20ResolutionModule + function decodeRequestData(bytes calldata _data) public pure returns (RequestParameters memory _params) { + _params = abi.decode(_data, (RequestParameters)); + } + + /// @inheritdoc IERC20ResolutionModule + function startResolution( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + escalations[_disputeId].startTime = block.timestamp; + emit VotingPhaseStarted(block.timestamp, _disputeId); + } + + /// @inheritdoc IERC20ResolutionModule + function castVote( + IOracle.Request calldata _request, + IOracle.Dispute calldata _dispute, + uint256 _numberOfVotes + ) public { + bytes32 _disputeId = _getId(_dispute); + if (ORACLE.createdAt(_disputeId) == 0) revert ERC20ResolutionModule_NonExistentDispute(); + if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.None) revert ERC20ResolutionModule_AlreadyResolved(); + + Escalation memory _escalation = escalations[_disputeId]; + if (_escalation.startTime == 0) revert ERC20ResolutionModule_DisputeNotEscalated(); + + RequestParameters memory _params = decodeRequestData(_request.resolutionModuleData); + uint256 _deadline = _escalation.startTime + _params.timeUntilDeadline; + if (block.timestamp >= _deadline) revert ERC20ResolutionModule_VotingPhaseOver(); + + votes[_disputeId][msg.sender] += _numberOfVotes; + + _voters[_disputeId].add(msg.sender); + escalations[_disputeId].totalVotes += _numberOfVotes; + + _params.votingToken.safeTransferFrom(msg.sender, address(this), _numberOfVotes); + emit VoteCast(msg.sender, _disputeId, _numberOfVotes); + } + + /// @inheritdoc IERC20ResolutionModule + function resolveDispute( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external onlyOracle { + // 0. Check disputeId actually exists and that it isn't resolved already + if (ORACLE.createdAt(_disputeId) == 0) revert ERC20ResolutionModule_NonExistentDispute(); + if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.None) revert ERC20ResolutionModule_AlreadyResolved(); + + Escalation memory _escalation = escalations[_disputeId]; + // 1. Check that the dispute is actually escalated + if (_escalation.startTime == 0) revert ERC20ResolutionModule_DisputeNotEscalated(); + + // 2. Check that voting deadline is over + RequestParameters memory _params = decodeRequestData(_request.resolutionModuleData); + uint256 _deadline = _escalation.startTime + _params.timeUntilDeadline; + if (block.timestamp < _deadline) revert ERC20ResolutionModule_OnGoingVotingPhase(); + + uint256 _quorumReached = _escalation.totalVotes >= _params.minVotesForQuorum ? 1 : 0; + + address[] memory __voters = _voters[_disputeId].values(); + + // 5. Update status + 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 _votersLength = __voters.length; + + // 6. Return tokens + for (uint256 _i; _i < _votersLength;) { + address _voter = __voters[_i]; + _params.votingToken.safeTransfer(_voter, votes[_disputeId][_voter]); + unchecked { + ++_i; + } + } + } + + /// @inheritdoc IERC20ResolutionModule + function getVoters(bytes32 _disputeId) external view returns (address[] memory __voters) { + __voters = _voters[_disputeId].values(); + } +} diff --git a/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol b/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol index 6e4bc7ad..58cb623b 100644 --- a/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol +++ b/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol @@ -1,142 +1,155 @@ -// // 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'; - -// /** -// * @title ERC20ResolutionModule -// * @notice This contract allows for disputes to be resolved by a voting process. -// * The voting process is started by the oracle and -// * the voting phase lasts for a certain amount of time. During this time, anyone can vote on the dispute. Once the voting -// * phase is over, the votes are tallied and if the votes in favor of the dispute are greater than the votes against the -// * dispute, the dispute is resolved in favor of the dispute. Otherwise, the dispute is resolved against the dispute. -// */ -// interface IERC20ResolutionModule is IResolutionModule { -// /*/////////////////////////////////////////////////////////////// -// EVENTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Emitted when a voter casts their vote on a dispute -// * @param _voter The address of the voter -// * @param _disputeId The id of the dispute -// * @param _numberOfVotes The number of votes cast by the voter -// */ -// event VoteCast(address _voter, bytes32 _disputeId, uint256 _numberOfVotes); - -// /** -// * @notice Emitted when the voting phase has started -// * @param _startTime The time when the voting phase started -// * @param _disputeId The ID of the dispute -// */ -// event VotingPhaseStarted(uint256 _startTime, bytes32 _disputeId); - -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Throws if the caller is not the dispute module -// */ -// error ERC20ResolutionModule_OnlyDisputeModule(); - -// /** -// * @notice Throws if the dispute has not been escalated -// */ -// error ERC20ResolutionModule_DisputeNotEscalated(); - -// /** -// * @notice Throws if the dispute is unresolved -// */ -// error ERC20ResolutionModule_UnresolvedDispute(); - -// /** -// * @notice Throws if the voting phase is over -// */ -// error ERC20ResolutionModule_VotingPhaseOver(); - -// /** -// * @notice Throws if the voting phase is ongoing -// */ -// error ERC20ResolutionModule_OnGoingVotingPhase(); - -// /** -// * @notice Throws if the dispute does not exist -// */ -// error ERC20ResolutionModule_NonExistentDispute(); - -// /** -// * @notice Throws if the dispute has already been resolved -// */ -// error ERC20ResolutionModule_AlreadyResolved(); - -// /*/////////////////////////////////////////////////////////////// -// STRUCTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Parameters of the request as stored in the module -// * @param votingToken The token used to vote -// * @param minVotesForQuorum The minimum amount of votes to win the dispute -// * @param timeUntilDeadline The time until the voting phase ends -// */ -// struct RequestParameters { -// IERC20 votingToken; -// uint256 minVotesForQuorum; -// uint256 timeUntilDeadline; -// } - -// /** -// * @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; -// } - -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @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); - -// function votes(bytes32 _disputeId, address _voter) external view returns (uint256 _votes); - -// /** -// * @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); - -// /// @inheritdoc IResolutionModule -// function startResolution(bytes32 _disputeId) external; - -// /** -// * @notice Casts a vote in favor of a dispute -// * @param _requestId The id of the request being disputed -// * @param _disputeId The id of the dispute being voted on -// * @param _numberOfVotes The number of votes to cast -// */ -// function castVote(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes) external; - -// /// @inheritdoc IResolutionModule -// function resolveDispute(bytes32 _disputeId) external; - -// /** -// * @notice Gets the voters of a dispute -// * @param _disputeId The id of the dispute -// * @return _voters The addresses of the voters -// */ -// function getVoters(bytes32 _disputeId) external view returns (address[] memory _voters); -// } +// 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'; + +/** + * @title ERC20ResolutionModule + * @notice This contract allows for disputes to be resolved by a voting process. + * The voting process is started by the oracle and + * the voting phase lasts for a certain amount of time. During this time, anyone can vote on the dispute. Once the voting + * phase is over, the votes are tallied and if the votes in favor of the dispute are greater than the votes against the + * dispute, the dispute is resolved in favor of the dispute. Otherwise, the dispute is resolved against the dispute. + */ +interface IERC20ResolutionModule is IResolutionModule { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Emitted when a voter casts their vote on a dispute + * @param _voter The address of the voter + * @param _disputeId The id of the dispute + * @param _numberOfVotes The number of votes cast by the voter + */ + event VoteCast(address _voter, bytes32 _disputeId, uint256 _numberOfVotes); + + /** + * @notice Emitted when the voting phase has started + * @param _startTime The time when the voting phase started + * @param _disputeId The ID of the dispute + */ + event VotingPhaseStarted(uint256 _startTime, bytes32 _disputeId); + + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Throws if the caller is not the dispute module + */ + error ERC20ResolutionModule_OnlyDisputeModule(); + + /** + * @notice Throws if the dispute has not been escalated + */ + error ERC20ResolutionModule_DisputeNotEscalated(); + + /** + * @notice Throws if the dispute is unresolved + */ + error ERC20ResolutionModule_UnresolvedDispute(); + + /** + * @notice Throws if the voting phase is over + */ + error ERC20ResolutionModule_VotingPhaseOver(); + + /** + * @notice Throws if the voting phase is ongoing + */ + error ERC20ResolutionModule_OnGoingVotingPhase(); + + /** + * @notice Throws if the dispute does not exist + */ + error ERC20ResolutionModule_NonExistentDispute(); + + /** + * @notice Throws if the dispute has already been resolved + */ + error ERC20ResolutionModule_AlreadyResolved(); + + /*/////////////////////////////////////////////////////////////// + STRUCTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Parameters of the request as stored in the module + * @param votingToken The token used to vote + * @param minVotesForQuorum The minimum amount of votes to win the dispute + * @param timeUntilDeadline The time until the voting phase ends + */ + struct RequestParameters { + IERC20 votingToken; + uint256 minVotesForQuorum; + uint256 timeUntilDeadline; + } + + /** + * @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; + } + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @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); + + function votes(bytes32 _disputeId, address _voter) external view returns (uint256 _votes); + + /** + * @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); + + /// @inheritdoc IResolutionModule + function startResolution( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external; + + /** + * @notice Casts a vote in favor of a dispute + * @param _numberOfVotes The number of votes to cast + */ + function castVote( + IOracle.Request calldata _request, + IOracle.Dispute calldata _dispute, + uint256 _numberOfVotes + ) external; + + /// @inheritdoc IResolutionModule + function resolveDispute( + bytes32 _disputeId, + IOracle.Request calldata _request, + IOracle.Response calldata _response, + IOracle.Dispute calldata _dispute + ) external; + + /** + * @notice Gets the voters of a dispute + * @param _disputeId The id of the dispute + * @return _voters The addresses of the voters + */ + function getVoters(bytes32 _disputeId) external view returns (address[] memory _voters); +} diff --git a/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol b/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol index da6dde87..dc87ae63 100644 --- a/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol +++ b/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol @@ -1,409 +1,400 @@ -// // 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 { -// ERC20ResolutionModule, -// IERC20ResolutionModule -// } from '../../../../contracts/modules/resolution/ERC20ResolutionModule.sol'; - -// contract ForTest_ERC20ResolutionModule is ERC20ResolutionModule { -// constructor(IOracle _oracle) ERC20ResolutionModule(_oracle) {} - -// function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { -// requestData[_requestId] = _data; -// } - -// function forTest_setEscalation(bytes32 _disputeId, ERC20ResolutionModule.Escalation calldata __escalation) public { -// escalations[_disputeId] = __escalation; -// } - -// function forTest_setVotes(bytes32 _disputeId, address _voter, uint256 _amountOfVotes) public { -// votes[_disputeId][_voter] = _amountOfVotes; -// } -// } - -// contract BaseTest is Test, Helpers { -// // The target contract -// ForTest_ERC20ResolutionModule public module; -// // A mock oracle -// IOracle public oracle; -// // 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 VoteCast(address _voter, bytes32 _disputeId, uint256 _numberOfVotes); -// event VotingPhaseStarted(uint256 _startTime, bytes32 _disputeId); -// event DisputeResolved(bytes32 indexed _requestId, bytes32 indexed _disputeId, IOracle.DisputeStatus _status); - -// /** -// * @notice Deploy the target and mock oracle extension -// */ -// function setUp() public { -// oracle = IOracle(makeAddr('Oracle')); -// vm.etch(address(oracle), hex'069420'); - -// token = IERC20(makeAddr('ERC20')); -// vm.etch(address(token), hex'069420'); - -// module = new ForTest_ERC20ResolutionModule(oracle); -// } - -// /** -// * @dev Helper function to cast 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)); -// vm.mockCall( -// address(token), -// abi.encodeCall(IERC20.transferFrom, (vm.addr(_i), address(module), _amountOfVotes)), -// abi.encode() -// ); -// module.castVote(_requestId, _disputeId, _amountOfVotes); -// vm.stopPrank(); -// _totalVotesCast += _amountOfVotes; -// unchecked { -// ++_i; -// } -// } -// } -// } - -// contract ERC20ResolutionModule_Unit_ModuleData is BaseTest { -// /** -// * @notice Test that the moduleName function returns the correct name -// */ -// function test_moduleName() public { -// assertEq(module.moduleName(), 'ERC20ResolutionModule'); -// } - -// /** -// * @notice Test that the decodeRequestData function returns the correct values -// */ -// function test_decodeRequestData_returnsCorrectData( -// bytes32 _requestId, -// address _token, -// uint256 _minVotesForQuorum, -// uint256 _votingTimeWindow -// ) public { -// // Mock data -// bytes memory _requestData = abi.encode(_token, _minVotesForQuorum, _votingTimeWindow); - -// // Store the mock request -// module.forTest_setRequestData(_requestId, _requestData); - -// // Test: decode the given request data -// IERC20ResolutionModule.RequestParameters memory _params = module.decodeRequestData(_requestId); - -// // Check: decoded values match original values? -// assertEq(address(_params.votingToken), _token); -// assertEq(_params.minVotesForQuorum, _minVotesForQuorum); -// assertEq(_params.timeUntilDeadline, _votingTimeWindow); -// } -// } - -// contract ERC20ResolutionModule_Unit_StartResolution is BaseTest { -// /** -// * @notice Test that the `startResolution` is correctly called and the voting phase is started -// */ -// function test_startResolution(bytes32 _disputeId) public { -// // Check: does revert if called by address != oracle? -// vm.expectRevert(IModule.Module_OnlyOracle.selector); -// module.startResolution(_disputeId); - -// // Check: emits VotingPhaseStarted event? -// vm.expectEmit(true, true, true, true); -// emit VotingPhaseStarted(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 ERC20ResolutionModule_Unit_CastVote is BaseTest { -// /** -// * @notice Test casting votes in valid voting time window. -// */ -// function test_castVote(bytes32 _requestId, bytes32 _disputeId, uint256 _amountOfVotes, address _voter) public { -// // Store mock 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)); - -// // Store mock escalation data with startTime 100_000 -// module.forTest_setEscalation( -// _disputeId, -// IERC20ResolutionModule.Escalation({ -// startTime: 100_000, -// totalVotes: 0 // Initial amount of votes -// }) -// ); - -// uint256 _minVotesForQuorum = 1; -// uint256 _votingTimeWindow = 40_000; - -// // Store mock request data with 40_000 voting time window -// module.forTest_setRequestData(_requestId, abi.encode(token, _minVotesForQuorum, _votingTimeWindow)); - -// // Mock and expect IERC20.transferFrom to be called -// _mockAndExpect( -// address(token), abi.encodeCall(IERC20.transferFrom, (_voter, address(module), _amountOfVotes)), abi.encode() -// ); - -// // Warp to voting phase -// vm.warp(130_000); - -// // Check: is the event emitted? -// vm.expectEmit(true, true, true, true); -// emit VoteCast(_voter, _disputeId, _amountOfVotes); - -// vm.prank(_voter); -// module.castVote(_requestId, _disputeId, _amountOfVotes); - -// (, uint256 _totalVotes) = module.escalations(_disputeId); -// // Check: totalVotes is updated? -// assertEq(_totalVotes, _amountOfVotes); - -// // Check: voter data is updated? -// assertEq(module.votes(_disputeId, _voter), _amountOfVotes); -// } - -// /** -// * @notice Test that `castVote` reverts if there is no dispute with the given`_disputeId` -// */ -// function test_revertIfNonExistentDispute(bytes32 _requestId, bytes32 _disputeId, uint256 _amountOfVotes) public { -// // Default non-existent dispute -// 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: reverts if called with `_disputeId` of a non-existent dispute? -// vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_NonExistentDispute.selector); -// module.castVote(_requestId, _disputeId, _amountOfVotes); -// } - -// /** -// * @notice Test that `castVote` reverts if called with `_disputeId` of a non-escalated dispute. -// */ -// function test_revertIfNotEscalated(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes) 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 called with `_disputeId` of a non-escalated dispute? -// vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_DisputeNotEscalated.selector); -// module.castVote(_requestId, _disputeId, _numberOfVotes); -// } - -// /** -// * @notice Test that `castVote` reverts if called with `_disputeId` of an already resolved dispute. -// */ -// function test_revertIfAlreadyResolved(bytes32 _requestId, bytes32 _disputeId, uint256 _amountOfVotes) 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: reverts if dispute is already resolved? -// vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_AlreadyResolved.selector); -// module.castVote(_requestId, _disputeId, _amountOfVotes); -// } - -// /** -// * @notice Test that `castVote` reverts if called outside the voting time window. -// */ -// function test_revertIfVotingPhaseOver( -// bytes32 _requestId, -// bytes32 _disputeId, -// uint256 _numberOfVotes, -// uint256 _timestamp -// ) public { -// vm.assume(_timestamp > 140_000); - -// // 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 -// vm.mockCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); - -// // Store request data -// uint256 _minVotesForQuorum = 1; -// uint256 _votingTimeWindow = 40_000; - -// module.forTest_setRequestData(_requestId, abi.encode(token, _minVotesForQuorum, _votingTimeWindow)); - -// // Jump to timestamp -// vm.warp(_timestamp); - -// // Check: reverts if trying to cast vote after voting phase? -// vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_VotingPhaseOver.selector); -// module.castVote(_requestId, _disputeId, _numberOfVotes); -// } -// } - -// contract ERC20ResolutionModule_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 -// vm.mockCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); - -// // Store request data -// uint256 _votingTimeWindow = 40_000; - -// module.forTest_setRequestData(_requestId, abi.encode(token, _minVotesForQuorum, _votingTimeWindow)); - -// // Store escalation data with `startTime` 100_000 and votes 0 -// module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.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(150_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 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 voting phase. -// */ -// function test_revertIfOnGoingVotePhase(bytes32 _requestId, bytes32 _disputeId, uint256 _timestamp) public { -// _timestamp = bound(_timestamp, 500_000, 999_999); - -// // 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, -// IERC20ResolutionModule.Escalation({ -// startTime: 500_000, -// totalVotes: 0 // Initial amount of votes -// }) -// ); - -// // Store request data -// uint256 _minVotesForQuorum = 1; -// uint256 _votingTimeWindow = 500_000; - -// module.forTest_setRequestData(_requestId, abi.encode(token, _minVotesForQuorum, _votingTimeWindow)); - -// // Jump to timestamp -// vm.warp(_timestamp); - -// // Check: reverts if trying to resolve during voting phase? -// vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_OnGoingVotingPhase.selector); -// vm.prank(address(oracle)); -// module.resolveDispute(_disputeId); -// } -// } - -// contract ERC20ResolutionModule_Unit_GetVoters is BaseTest { -// /** -// * @notice Test that `getVoters` returns an array of addresses of users that have voted. -// */ -// function test_getVoters(bytes32 _requestId, bytes32 _disputeId) 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 _votingTimeWindow = 40_000; -// uint256 _minVotesForQuorum = 1; - -// module.forTest_setRequestData(_requestId, abi.encode(token, _minVotesForQuorum, _votingTimeWindow)); - -// // Store escalation data with `startTime` 100_000 and votes 0 -// module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); - -// uint256 _votersAmount = 3; - -// // Make 3 addresses cast 100 votes each -// _populateVoters(_requestId, _disputeId, _votersAmount, 100); - -// address[] memory _votersArray = module.getVoters(_disputeId); - -// for (uint256 _i = 1; _i <= _votersAmount; _i++) { -// assertEq(_votersArray[_i - 1], vm.addr(_i)); -// } -// } -// } +// 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 { + ERC20ResolutionModule, + IERC20ResolutionModule +} from '../../../../contracts/modules/resolution/ERC20ResolutionModule.sol'; + +contract ForTest_ERC20ResolutionModule is ERC20ResolutionModule { + constructor(IOracle _oracle) ERC20ResolutionModule(_oracle) {} + + function forTest_setEscalation(bytes32 _disputeId, ERC20ResolutionModule.Escalation calldata __escalation) public { + escalations[_disputeId] = __escalation; + } + + function forTest_setVotes(bytes32 _disputeId, address _voter, uint256 _amountOfVotes) public { + votes[_disputeId][_voter] = _amountOfVotes; + } +} + +contract BaseTest is Test, Helpers { + // The target contract + ForTest_ERC20ResolutionModule public module; + // A mock oracle + IOracle public oracle; + // 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; + address internal _proposer = makeAddr('proposer'); + bytes32 public mockId = bytes32('69'); + + // Mocking module events + event VoteCast(address _voter, bytes32 _disputeId, uint256 _numberOfVotes); + event VotingPhaseStarted(uint256 _startTime, bytes32 _disputeId); + event DisputeResolved(bytes32 indexed _requestId, bytes32 indexed _disputeId, IOracle.DisputeStatus _status); + + /** + * @notice Deploy the target and mock oracle extension + */ + function setUp() public { + oracle = IOracle(makeAddr('Oracle')); + vm.etch(address(oracle), hex'069420'); + + token = IERC20(makeAddr('ERC20')); + vm.etch(address(token), hex'069420'); + + module = new ForTest_ERC20ResolutionModule(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 cast 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)); + vm.mockCall( + address(token), + abi.encodeCall(IERC20.transferFrom, (vm.addr(_i), address(module), _amountOfVotes)), + abi.encode() + ); + module.castVote(_request, mockDispute, _amountOfVotes); + vm.stopPrank(); + _totalVotesCast += _amountOfVotes; + unchecked { + ++_i; + } + } + } +} + +contract ERC20ResolutionModule_Unit_ModuleData is BaseTest { + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleName() public { + assertEq(module.moduleName(), 'ERC20ResolutionModule'); + } + + /** + * @notice Test that the decodeRequestData function returns the correct values + */ + function test_decodeRequestData_returnsCorrectData( + bytes32 _requestId, + address _token, + uint256 _minVotesForQuorum, + uint256 _votingTimeWindow + ) public { + // Mock data + bytes memory _requestData = abi.encode(_token, _minVotesForQuorum, _votingTimeWindow); + + // Test: decode the given request data + IERC20ResolutionModule.RequestParameters memory _params = module.decodeRequestData(_requestData); + + // Check: decoded values match original values? + assertEq(address(_params.votingToken), _token); + assertEq(_params.minVotesForQuorum, _minVotesForQuorum); + assertEq(_params.timeUntilDeadline, _votingTimeWindow); + } +} + +contract ERC20ResolutionModule_Unit_StartResolution is BaseTest { + /** + * @notice Test that the `startResolution` is correctly called and the voting phase is started + */ + function test_startResolution(bytes32 _disputeId, IOracle.Request calldata _request) public { + // Check: does revert if called by address != oracle? + vm.expectRevert(IModule.Module_OnlyOracle.selector); + module.startResolution(_disputeId, _request, mockResponse, mockDispute); + + // Check: emits VotingPhaseStarted event? + vm.expectEmit(true, true, true, true); + emit VotingPhaseStarted(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 ERC20ResolutionModule_Unit_CastVote is BaseTest { + /** + * @notice Test casting votes in valid voting time window. + */ + function test_castVote( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _amountOfVotes, + address _voter, + IOracle.Request calldata _request + ) public { + // Store mock dispute + mockDispute.requestId = _getId(_request); + + // Store mock escalation data with startTime 100_000 + module.forTest_setEscalation( + _disputeId, + IERC20ResolutionModule.Escalation({ + startTime: 100_000, + totalVotes: 0 // Initial amount of votes + }) + ); + + uint256 _minVotesForQuorum = 1; + uint256 _votingTimeWindow = 40_000; + + // Mock and expect IERC20.transferFrom to be called + _mockAndExpect( + address(token), abi.encodeCall(IERC20.transferFrom, (_voter, address(module), _amountOfVotes)), abi.encode() + ); + + // Warp to voting phase + vm.warp(130_000); + + // Check: is the event emitted? + vm.expectEmit(true, true, true, true); + emit VoteCast(_voter, _disputeId, _amountOfVotes); + + vm.prank(_voter); + module.castVote(_request, mockDispute, _amountOfVotes); + + (, uint256 _totalVotes) = module.escalations(_disputeId); + // Check: totalVotes is updated? + assertEq(_totalVotes, _amountOfVotes); + + // Check: voter data is updated? + assertEq(module.votes(_disputeId, _voter), _amountOfVotes); + } + + /** + * @notice Test that `castVote` reverts if there is no dispute with the given`_disputeId` + */ + function test_revertIfNonExistentDispute( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _amountOfVotes, + IOracle.Request calldata _request + ) public { + // Default non-existent dispute + mockDispute.disputer = address(0); + mockDispute.responseId = bytes32(0); + mockDispute.proposer = address(0); + mockDispute.requestId = bytes32(0); + + // Check: reverts if called with `_disputeId` of a non-existent dispute? + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_NonExistentDispute.selector); + module.castVote(_request, mockDispute, _amountOfVotes); + } + + /** + * @notice Test that `castVote` reverts if called with `_disputeId` of a non-escalated dispute. + */ + function test_revertIfNotEscalated( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _numberOfVotes, + IOracle.Request calldata _request + ) public { + // Mock the oracle response for looking up a dispute + mockDispute.requestId = _requestId; + + // Check: reverts if called with `_disputeId` of a non-escalated dispute? + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_DisputeNotEscalated.selector); + module.castVote(_request, mockDispute, _numberOfVotes); + } + + /** + * @notice Test that `castVote` reverts if called with `_disputeId` of an already resolved dispute. + */ + function test_revertIfAlreadyResolved( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _amountOfVotes, + IOracle.Request calldata _request + ) public { + // Mock dispute already resolved => DisputeStatus.Lost + mockDispute.requestId = _requestId; + + // Check: reverts if dispute is already resolved? + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_AlreadyResolved.selector); + module.castVote(_request, mockDispute, _amountOfVotes); + } + + /** + * @notice Test that `castVote` reverts if called outside the voting time window. + */ + function test_revertIfVotingPhaseOver( + bytes32 _requestId, + bytes32 _disputeId, + uint256 _numberOfVotes, + uint256 _timestamp, + IOracle.Request calldata _request + ) public { + vm.assume(_timestamp > 140_000); + + // Mock the oracle response for looking up a dispute + mockDispute.requestId = _requestId; + + module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); + + // Store request data + uint256 _minVotesForQuorum = 1; + uint256 _votingTimeWindow = 40_000; + + // Jump to timestamp + vm.warp(_timestamp); + + // Check: reverts if trying to cast vote after voting phase? + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_VotingPhaseOver.selector); + module.castVote(_request, mockDispute, _numberOfVotes); + } +} + +contract ERC20ResolutionModule_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(IOracle.Request calldata _request, bytes32 _disputeId, uint16 _minVotesForQuorum) public { + // Store mock dispute and mock calls + bytes32 _requestId = _getId(_request); + mockDispute.requestId = _requestId; + + // Store request data + uint256 _votingTimeWindow = 40_000; + + // Store escalation data with `startTime` 100_000 and votes 0 + module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.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(150_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 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 voting phase. + */ + function test_revertIfOnGoingVotePhase( + IOracle.Request calldata _request, + bytes32 _disputeId, + uint256 _timestamp + ) public { + _timestamp = bound(_timestamp, 500_000, 999_999); + + // Store mock dispute and mock calls + bytes32 _requestId = _getId(_request); + mockDispute.requestId = _requestId; + + module.forTest_setEscalation( + _disputeId, + IERC20ResolutionModule.Escalation({ + startTime: 500_000, + totalVotes: 0 // Initial amount of votes + }) + ); + + // Store request data + uint256 _minVotesForQuorum = 1; + uint256 _votingTimeWindow = 500_000; + + // Jump to timestamp + vm.warp(_timestamp); + + // Check: reverts if trying to resolve during voting phase? + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_OnGoingVotingPhase.selector); + vm.prank(address(oracle)); + module.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + } +} + +contract ERC20ResolutionModule_Unit_GetVoters is BaseTest { + /** + * @notice Test that `getVoters` returns an array of addresses of users that have voted. + */ + function test_getVoters(IOracle.Request calldata _request, bytes32 _disputeId) public { + // Store mock dispute and mock calls + bytes32 _requestId = _getId(_request); + mockDispute.requestId = _requestId; + + // Store request data + uint256 _votingTimeWindow = 40_000; + uint256 _minVotesForQuorum = 1; + + // Store escalation data with `startTime` 100_000 and votes 0 + module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); + uint256 _votersAmount = 3; + + // Make 3 addresses cast 100 votes each + _populateVoters(_requestId, _disputeId, _votersAmount, 100, _request); + + address[] memory _votersArray = module.getVoters(_disputeId); + + for (uint256 _i = 1; _i <= _votersAmount; _i++) { + assertEq(_votersArray[_i - 1], vm.addr(_i)); + } + } +}