From e025c125e5e1972c19f676d63ffc5e47d760c6e4 Mon Sep 17 00:00:00 2001 From: 0xng <87835144+0xng@users.noreply.github.com> Date: Fri, 23 Jun 2023 07:47:01 +0100 Subject: [PATCH] feat: bond escalation resolution (#30) --- .../extensions/BondEscalationAccounting.sol | 1 + .../BondEscalationResolutionModule.sol | 430 ++++++++++ .../IBondEscalationResolutionModule.sol | 78 ++ .../unit/BondEscalationResolutionModule.t.sol | 775 ++++++++++++++++++ 4 files changed, 1284 insertions(+) create mode 100644 solidity/contracts/modules/BondEscalationResolutionModule.sol create mode 100644 solidity/interfaces/modules/IBondEscalationResolutionModule.sol create mode 100644 solidity/test/unit/BondEscalationResolutionModule.t.sol diff --git a/solidity/contracts/extensions/BondEscalationAccounting.sol b/solidity/contracts/extensions/BondEscalationAccounting.sol index 8a6ad979..4d7f8af5 100644 --- a/solidity/contracts/extensions/BondEscalationAccounting.sol +++ b/solidity/contracts/extensions/BondEscalationAccounting.sol @@ -39,6 +39,7 @@ contract BondEscalationAccounting is AccountingExtension, IBondEscalationAccount pledges[_requestId][_disputeId][_token] += _amount; } + // TODO: add _disputeId parameter emit Pledge(_pledger, _requestId, _token, _amount); } diff --git a/solidity/contracts/modules/BondEscalationResolutionModule.sol b/solidity/contracts/modules/BondEscalationResolutionModule.sol new file mode 100644 index 00000000..f258cf00 --- /dev/null +++ b/solidity/contracts/modules/BondEscalationResolutionModule.sol @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {IBondEscalationResolutionModule} from '../../interfaces/modules/IBondEscalationResolutionModule.sol'; +import {IOracle} from '../../interfaces/IOracle.sol'; +import {IDisputeModule} from '../../interfaces/modules/IDisputeModule.sol'; +import {IBondEscalationAccounting} from '../../interfaces/extensions/IBondEscalationAccounting.sol'; +import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; + +import {Module} from '../Module.sol'; + +contract BondEscalationResolutionModule is Module, IBondEscalationResolutionModule { + using SafeERC20 for IERC20; + + uint256 public constant BASE = 100; + + mapping(bytes32 _disputeId => EscalationData _escalationData) public escalationData; + mapping(bytes32 _disputeId => InequalityData _inequalityData) public inequalityData; + + mapping(bytes32 _disputeId => PledgeData[] _pledgeData) public pledgedFor; + mapping(bytes32 _disputeId => PledgeData[] _pledgeData) public pledgedAgainst; + + constructor(IOracle _oracle) Module(_oracle) {} + + function moduleName() external pure returns (string memory _moduleName) { + return 'BondEscalationResolutionModule'; + } + + function decodeRequestData(bytes32 _requestId) + public + view + returns ( + IBondEscalationAccounting _accountingExtension, + IERC20 _token, + uint256 _percentageDiff, + uint256 _pledgeThreshold, + uint256 _timeUntilDeadline, + uint256 _timeToBreakInequality + ) + { + (_accountingExtension, _token, _percentageDiff, _pledgeThreshold, _timeUntilDeadline, _timeToBreakInequality) = + _decodeRequestData(requestData[_requestId]); + } + + function _decodeRequestData(bytes memory _data) + internal + pure + returns ( + IBondEscalationAccounting _accountingExtension, + IERC20 _token, + uint256 _percentageDiff, + uint256 _pledgeThreshold, + uint256 _timeUntilDeadline, + uint256 _timeToBreakInequality + ) + { + (_accountingExtension, _token, _percentageDiff, _pledgeThreshold, _timeUntilDeadline, _timeToBreakInequality) = + abi.decode(_data, (IBondEscalationAccounting, IERC20, uint256, uint256, uint256, uint256)); + } + + function escalateDispute(bytes32 _disputeId) external { + bytes32 _requestId = ORACLE.getDispute(_disputeId).requestId; + IDisputeModule _disputeModule = ORACLE.getRequest(_requestId).disputeModule; + if (msg.sender != address(_disputeModule)) revert BondEscalationResolutionModule_OnlyDisputeModule(); + escalationData[_disputeId].startTime = uint128(block.timestamp); + emit DisputeEscalated(_disputeId, _requestId); + } + + function pledgeForDispute(bytes32 _requestId, bytes32 _disputeId, uint256 _pledgeAmount) external { + // Cache reused struct + EscalationData storage _escalationData = escalationData[_disputeId]; + + // Revert if dispute not escalated + if (_escalationData.startTime == 0) revert BondEscalationResolutionModule_NotEscalated(); + + InequalityData storage _inequalityData = inequalityData[_disputeId]; + + { + // Get necessary params + (,,,, uint256 _timeUntilDeadline, uint256 _timeToBreakInequality) = decodeRequestData(_requestId); + + // Calculate deadline + // TODO: check overflow + uint256 _pledgingDeadline = _escalationData.startTime + _timeUntilDeadline; + + // Revert if we are in or past the deadline + if (block.timestamp >= _pledgingDeadline) revert BondEscalationResolutionModule_PledgingPhaseOver(); + + // Check + if (_inequalityData.time != 0 && block.timestamp >= _inequalityData.time + _timeToBreakInequality) { + revert BondEscalationResolutionModule_MustBeResolved(); + } + + if (_inequalityData.inequalityStatus == InequalityStatus.AgainstTurnToEqualize) { + revert BondEscalationResolutionModule_AgainstTurnToEqualize(); + } + } + + uint256 _currentForVotes = _escalationData.pledgesFor; + uint256 _currentAgainstVotes = _escalationData.pledgesAgainst; + + // Refetching to avoid stack-too-deep + (IBondEscalationAccounting _accountingExtension, IERC20 _token, uint256 _percentageDiff, uint256 _pledgeThreshold,,) + = decodeRequestData(_requestId); + + // If minThreshold not reached, or ForTurnToVote, or Equalized allow vote + if ( + _currentForVotes < _pledgeThreshold && _currentAgainstVotes < _pledgeThreshold + || _inequalityData.inequalityStatus == InequalityStatus.Equalized + || _inequalityData.inequalityStatus == InequalityStatus.ForTurnToEqualize + ) { + // Optimistically update amount of votes pledged for the dispute + _escalationData.pledgesFor += _pledgeAmount; + + // Optimistically update amount of votes pledged by the caller + // TODO: change to a better data structure -- set/dictionary + pledgedFor[_disputeId].push(PledgeData({pledger: msg.sender, pledges: _pledgeAmount})); + + // Pledge in the accounting extension + _accountingExtension.pledge(msg.sender, _requestId, _disputeId, _token, _pledgeAmount); + + // Emit event + emit PledgedForDispute(msg.sender, _requestId, _disputeId, _pledgeAmount); + + // Update InequalityData accordingly + uint256 _updatedForVotes = _currentForVotes + _pledgeAmount; + + // If the new amount of pledged votes doesn't surpass the threshold, return + if ( + _currentForVotes < _pledgeThreshold && _currentAgainstVotes < _pledgeThreshold + && _updatedForVotes < _pledgeThreshold + ) { + return; + } + + uint256 _currentTotalVotes = _currentForVotes + _currentAgainstVotes; + // TODO: add larger coefficient + uint256 _currentForVotesPercentage = _updatedForVotes * 100 / _currentTotalVotes; + uint256 _currentAgainstVotesPercentage = _currentAgainstVotes * 100 / _currentTotalVotes; + // TODO: check math + int256 _forPercentageDifference = int256(_currentForVotesPercentage) - int256(_currentAgainstVotesPercentage); + int256 _againstPercentageDifference = int256(_currentAgainstVotesPercentage) - int256(_currentForVotesPercentage); + + // TODO: safe cast? it should never reach max tho + int256 _percentageDiffAsInt = int256(_percentageDiff); + + if ( + _currentForVotes < _pledgeThreshold && _currentAgainstVotes < _pledgeThreshold + && _updatedForVotes >= _pledgeThreshold && _forPercentageDifference < _percentageDiffAsInt + ) { + _inequalityData.inequalityStatus = InequalityStatus.Equalized; + _inequalityData.time = 0; + return; + } + + if (_forPercentageDifference >= _percentageDiffAsInt) { + _inequalityData.inequalityStatus = InequalityStatus.AgainstTurnToEqualize; + _inequalityData.time = block.timestamp; + return; + } + + // If the difference is still below the equalization threshold, leave as ForTurnToEqualize + if ( + _againstPercentageDifference >= _percentageDiffAsInt + && _inequalityData.inequalityStatus == InequalityStatus.ForTurnToEqualize + ) { + return; + } + + // If it was the time of the for equalizers to equalize, and they did, reset the timer + if ( + _againstPercentageDifference < _percentageDiffAsInt + && _inequalityData.inequalityStatus == InequalityStatus.ForTurnToEqualize + ) { + _inequalityData.inequalityStatus = InequalityStatus.Equalized; + _inequalityData.time = 0; + return; + } + } + } + + function pledgeAgainstDispute(bytes32 _requestId, bytes32 _disputeId, uint256 _pledgeAmount) external { + // Cache reused struct + EscalationData storage _escalationData = escalationData[_disputeId]; + + // Revert if dispute not escalated + if (_escalationData.startTime == 0) revert BondEscalationResolutionModule_NotEscalated(); + + InequalityData storage _inequalityData = inequalityData[_disputeId]; + { + // Get necessary params + (,,,, uint256 _timeUntilDeadline, uint256 _timeToBreakInequality) = decodeRequestData(_requestId); + + // Calculate deadline + // TODO: check overflow + uint256 _pledgingDeadline = _escalationData.startTime + _timeUntilDeadline; + + // Revert if we are in or past the deadline + if (block.timestamp >= _pledgingDeadline) revert BondEscalationResolutionModule_PledgingPhaseOver(); + + // Check + if (_inequalityData.time != 0 && block.timestamp >= _inequalityData.time + _timeToBreakInequality) { + revert BondEscalationResolutionModule_MustBeResolved(); + } + + if (_inequalityData.inequalityStatus == InequalityStatus.ForTurnToEqualize) { + revert BondEscalationResolutionModule_ForTurnToEqualize(); + } + } + + uint256 _currentForVotes = _escalationData.pledgesFor; + uint256 _currentAgainstVotes = _escalationData.pledgesAgainst; + + // Refetching to avoid stack-too-deep + (IBondEscalationAccounting _accountingExtension, IERC20 _token, uint256 _percentageDiff, uint256 _pledgeThreshold,,) + = decodeRequestData(_requestId); + + // If minThreshold not reached, allow vote + if ( + _currentForVotes < _pledgeThreshold && _currentAgainstVotes < _pledgeThreshold + || _inequalityData.inequalityStatus == InequalityStatus.Equalized + || _inequalityData.inequalityStatus == InequalityStatus.AgainstTurnToEqualize + ) { + // Optimistically update amount of votes pledged for the dispute + _escalationData.pledgesAgainst += _pledgeAmount; + + // Optimistically update amount of votes pledged by the caller + pledgedAgainst[_disputeId].push(PledgeData({pledger: msg.sender, pledges: _pledgeAmount})); + + // Pledge in the accounting extension + _accountingExtension.pledge(msg.sender, _requestId, _disputeId, _token, _pledgeAmount); + + // Emit event + emit PledgedAgainstDispute(msg.sender, _requestId, _disputeId, _pledgeAmount); + + // Update InequalityData accordingly + uint256 _updatedAgainstVotes = _currentAgainstVotes + _pledgeAmount; + + // If the new amount of pledged votes doesn't surpass the threshold, return + if ( + _currentForVotes < _pledgeThreshold && _currentAgainstVotes < _pledgeThreshold + && _updatedAgainstVotes < _pledgeThreshold + ) { + return; + } + + uint256 _currentTotalVotes = _currentForVotes + _currentAgainstVotes; + // TODO: add larger coefficient + uint256 _currentForVotesPercentage = _currentForVotes * 100 / _currentTotalVotes; + uint256 _currentAgainstVotesPercentage = _updatedAgainstVotes * 100 / _currentTotalVotes; + // TODO: check math + int256 _forPercentageDifference = int256(_currentForVotesPercentage) - int256(_currentAgainstVotesPercentage); + int256 _againstPercentageDifference = int256(_currentAgainstVotesPercentage) - int256(_currentForVotesPercentage); + + // TODO: safe cast? it should never reach max tho + int256 _percentageDiffAsInt = int256(_percentageDiff); + + if ( + _currentForVotes < _pledgeThreshold && _currentAgainstVotes < _pledgeThreshold + && _updatedAgainstVotes >= _pledgeThreshold && _againstPercentageDifference < _percentageDiffAsInt + ) { + _inequalityData.inequalityStatus = InequalityStatus.Equalized; + _inequalityData.time = 0; + return; + } + + if (_againstPercentageDifference >= _percentageDiffAsInt) { + _inequalityData.inequalityStatus = InequalityStatus.ForTurnToEqualize; + _inequalityData.time = block.timestamp; + return; + } + + // If the difference is still below the equalization threshold, leave as AgainstTurnToEqualize + if ( + _forPercentageDifference >= _percentageDiffAsInt + && _inequalityData.inequalityStatus == InequalityStatus.AgainstTurnToEqualize + ) { + return; + } + + // If it was the time of the for equalizers to equalize, and they did, reset the timer + if ( + _forPercentageDifference < _percentageDiffAsInt + && _inequalityData.inequalityStatus == InequalityStatus.AgainstTurnToEqualize + ) { + _inequalityData.inequalityStatus = InequalityStatus.Equalized; + _inequalityData.time = 0; + return; + } + } + } + + function resolveDispute(bytes32 _disputeId) external { + // Cache reused struct + EscalationData storage _escalationData = escalationData[_disputeId]; + + // Revert if already resolved + if (_escalationData.resolution != Resolution.Unresolved) revert BondEscalationResolutionModule_AlreadyResolved(); + + // Revert if dispute not escalated + if (_escalationData.startTime == 0) revert BondEscalationResolutionModule_NotEscalated(); + + // Get requestId + bytes32 _requestId = ORACLE.getDispute(_disputeId).requestId; + + // Get necessary params + (,,, uint256 _pledgeThreshold, uint256 _timeUntilDeadline, uint256 _timeToBreakInequality) = + decodeRequestData(_requestId); + + // Cache reused inequality data + InequalityData storage _inequalityData = inequalityData[_disputeId]; + + // TODO: 0 check on .time? I guess it may be necessary due to potential misconfiguration + // if _timeToBreakInequality > block.timestamp this could be settled instantly + uint256 _inequalityTimerDeadline = _inequalityData.time + _timeToBreakInequality; + + // Calculate deadline + // TODO: check overflow + uint256 _pledgingDeadline = _escalationData.startTime + _timeUntilDeadline; + + // Revert if we have not yet reached the deadline and the timer has not passed + // TODO: double check this when fresh + if ( + block.timestamp < _pledgingDeadline && _inequalityTimerDeadline != 0 && block.timestamp < _inequalityTimerDeadline + ) revert BondEscalationResolutionModule_PledgingPhaseNotOver(); + + // TODO: cache variables + if ( + _escalationData.pledgesFor < _pledgeThreshold && _escalationData.pledgesAgainst < _pledgeThreshold + || _escalationData.pledgesFor == _escalationData.pledgesAgainst + ) { + _escalationData.resolution = Resolution.NoResolution; + // TODO: + // ORACLE.updateDisputeStatus(_disputeId, IOracle.DisputeStatus.NoResolution); + // emit DisputeResolved(_disputeId, IOracle.DisputeStatus.NoResolution); + // return; + } + + if (_escalationData.pledgesFor > _escalationData.pledgesAgainst) { + _escalationData.resolution = Resolution.DisputerWon; + ORACLE.updateDisputeStatus(_disputeId, IOracle.DisputeStatus.Won); + emit DisputeResolved(_disputeId, IOracle.DisputeStatus.Won); + return; + } + + if (_escalationData.pledgesAgainst > _escalationData.pledgesFor) { + _escalationData.resolution = Resolution.DisputerLost; + ORACLE.updateDisputeStatus(_disputeId, IOracle.DisputeStatus.Lost); + emit DisputeResolved(_disputeId, IOracle.DisputeStatus.Lost); + return; + } + } + + // TODO: Note: It's possible that because we are using the dispute module and the resolution module with + // the same extension, that the balance for this dispute increases due to both using them. + // it's extremely important to be careful with math precision here to not DoS the accountancy settling + function settleAccountancy(bytes32 _requestId, bytes32 _disputeId) external { + EscalationData storage _escalationData = escalationData[_disputeId]; + + // Revert if not resolved + if (_escalationData.resolution == Resolution.Unresolved) revert BondEscalationResolutionModule_NotResolved(); + + uint256 _pledgesForLength = pledgedFor[_disputeId].length; + uint256 _pledgesAgainstLength = pledgedAgainst[_disputeId].length; + + (IBondEscalationAccounting _accountingExtension, IERC20 _token,,,,) = decodeRequestData(_requestId); + + if (_escalationData.resolution == Resolution.DisputerWon) { + // TODO: check math -- add coefficient + uint256 _amountPerPledger = _escalationData.pledgesAgainst / _pledgesForLength; + // TODO: lmao improve this with enumerable set or some thist + address[] memory _winningPledgers = new address[](_pledgesForLength); + for (uint256 _i; _i < _pledgesForLength;) { + _winningPledgers[_i] = pledgedFor[_disputeId][_i].pledger; + unchecked { + ++_i; + } + } + _accountingExtension.payWinningPledgers(_requestId, _disputeId, _winningPledgers, _token, _amountPerPledger); + // TODO: emit event + return; + } + + if (_escalationData.resolution == Resolution.DisputerLost) { + // TODO: check math -- add coefficient + uint256 _amountPerPledger = _escalationData.pledgesFor / _pledgesAgainstLength; + // TODO: lmao improve this with enumerable set or some thist + address[] memory _winningPledgers = new address[](_pledgesAgainstLength); + for (uint256 _i; _i < _pledgesAgainstLength;) { + _winningPledgers[_i] = pledgedAgainst[_disputeId][_i].pledger; + unchecked { + ++_i; + } + } + _accountingExtension.payWinningPledgers(_requestId, _disputeId, _winningPledgers, _token, _amountPerPledger); + // TODO: emit event + return; + } + + // TODO: add NoResolution release path + } + + function fetchPledgeDataFor(bytes32 _disputeId) external view returns (PledgeData[] memory _pledgeData) { + PledgeData[] memory _pledgeDataCache = pledgedFor[_disputeId]; + uint256 _pledgedForLength = _pledgeDataCache.length; + _pledgeData = new PledgeData[](_pledgedForLength); + + for (uint256 _i; _i < _pledgedForLength;) { + _pledgeData[_i] = _pledgeDataCache[_i]; + unchecked { + ++_i; + } + } + } + + function fetchPledgeDataAgainst(bytes32 _disputeId) external view returns (PledgeData[] memory _pledgeData) { + PledgeData[] memory _pledgeDataCache = pledgedAgainst[_disputeId]; + uint256 _pledgedAgainstLength = _pledgeDataCache.length; + _pledgeData = new PledgeData[](_pledgedAgainstLength); + + for (uint256 _i; _i < _pledgedAgainstLength;) { + _pledgeData[_i] = _pledgeDataCache[_i]; + unchecked { + ++_i; + } + } + } +} diff --git a/solidity/interfaces/modules/IBondEscalationResolutionModule.sol b/solidity/interfaces/modules/IBondEscalationResolutionModule.sol new file mode 100644 index 00000000..83c6adc2 --- /dev/null +++ b/solidity/interfaces/modules/IBondEscalationResolutionModule.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.16 <0.9.0; + +import {IOracle} from '../IOracle.sol'; +import {IResolutionModule} from './IResolutionModule.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IBondEscalationAccounting} from '../extensions/IBondEscalationAccounting.sol'; + +interface IBondEscalationResolutionModule is IResolutionModule { + enum InequalityStatus { + Unstarted, + Equalized, + ForTurnToEqualize, + AgainstTurnToEqualize + } + + enum Resolution { + Unresolved, + DisputerWon, + DisputerLost, + NoResolution + } + + struct PledgeData { + address pledger; + uint256 pledges; + } + + struct InequalityData { + InequalityStatus inequalityStatus; + uint256 time; + } + + struct EscalationData { + Resolution resolution; + uint128 startTime; + uint256 pledgesFor; + uint256 pledgesAgainst; + } + + // TODO: should I add requestId as a param? + event DisputeResolved(bytes32 indexed _disputeId, IOracle.DisputeStatus _status); + event DisputeEscalated(bytes32 indexed _disputeId, bytes32 indexed requestId); + event PledgedForDispute( + address indexed _pledger, bytes32 indexed _requestId, bytes32 indexed _disputeId, uint256 _pledgedAmount + ); + event PledgedAgainstDispute( + address indexed _pledger, bytes32 indexed _requestId, bytes32 indexed _disputeId, uint256 _pledgedAmount + ); + + error BondEscalationResolutionModule_OnlyDisputeModule(); + error BondEscalationResolutionModule_AlreadyResolved(); + error BondEscalationResolutionModule_NotResolved(); + error BondEscalationResolutionModule_NotEscalated(); + error BondEscalationResolutionModule_PledgingPhaseOver(); + error BondEscalationResolutionModule_PledgingPhaseNotOver(); + error BondEscalationResolutionModule_MustBeResolved(); + error BondEscalationResolutionModule_AgainstTurnToEqualize(); + error BondEscalationResolutionModule_ForTurnToEqualize(); + + function escalationData(bytes32 _disputeId) + external + view + returns (Resolution _resolution, uint128 _startTime, uint256 _votesFor, uint256 _votesAgainst); + function inequalityData(bytes32 _disputeId) external view returns (InequalityStatus _inequalityStatus, uint256 _time); + + function decodeRequestData(bytes32 _requestId) + external + view + returns ( + IBondEscalationAccounting _accountingExtension, + IERC20 _token, + uint256 _percentageDiff, + uint256 _pledgeThreshold, + uint256 _timeUntilDeadline, + uint256 _timeToBreakInequality + ); +} diff --git a/solidity/test/unit/BondEscalationResolutionModule.t.sol b/solidity/test/unit/BondEscalationResolutionModule.t.sol new file mode 100644 index 00000000..c4ac9afd --- /dev/null +++ b/solidity/test/unit/BondEscalationResolutionModule.t.sol @@ -0,0 +1,775 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +// solhint-disable-next-line +import 'forge-std/Test.sol'; + +import { + BondEscalationResolutionModule, + Module, + IOracle, + IBondEscalationAccounting, + IBondEscalationResolutionModule, + IERC20 +} from '../../contracts/modules/BondEscalationResolutionModule.sol'; + +import {IRequestModule} from '../../interfaces/modules/IRequestModule.sol'; +import {IResponseModule} from '../../interfaces/modules/IResponseModule.sol'; +import {IDisputeModule} from '../../interfaces/modules/IDisputeModule.sol'; +import {IResolutionModule} from '../../interfaces/modules/IResolutionModule.sol'; +import {IFinalityModule} from '../../interfaces/modules/IFinalityModule.sol'; + +import {IModule} from '../../contracts/Module.sol'; +import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; + +import {Strings} from '@openzeppelin/contracts/utils/Strings.sol'; + +/** + * @dev Harness to set an entry in the requestData mapping, without triggering setup request hooks + */ +contract ForTest_BondEscalationResolutionModule is BondEscalationResolutionModule { + constructor(IOracle _oracle) BondEscalationResolutionModule(_oracle) {} + + function forTest_setRequestData(bytes32 _requestId, bytes memory _data) public { + requestData[_requestId] = _data; + } + + function forTest_setEscalationData( + bytes32 _disputeId, + BondEscalationResolutionModule.EscalationData calldata __escalationData + ) public { + escalationData[_disputeId] = __escalationData; + } + + function forTest_setInequalityData( + bytes32 _disputeId, + BondEscalationResolutionModule.InequalityData calldata _inequalityData + ) public { + inequalityData[_disputeId] = _inequalityData; + } + + function forTest_setPledgedFor(bytes32 _disputeId, address[] calldata _pledgers, uint256[] calldata _pledges) public { + require(_pledgers.length == _pledges.length, 'mismatched lengths'); + + for (uint256 _i; _i < _pledgers.length; _i++) { + pledgedFor[_disputeId].push(IBondEscalationResolutionModule.PledgeData(_pledgers[_i], _pledges[_i])); + } + } + + function forTest_setPledgedAgainst( + bytes32 _disputeId, + address[] calldata _pledgers, + uint256[] calldata _pledges + ) public { + require(_pledgers.length == _pledges.length, 'mismatched lengths'); + + for (uint256 _i; _i < _pledgers.length; _i++) { + pledgedAgainst[_disputeId].push(IBondEscalationResolutionModule.PledgeData(_pledgers[_i], _pledges[_i])); + } + } +} + +/** + * @title Bonded Escalation Resolution Module Unit tests + */ + +contract BondEscalationResolutionModule_UnitTest is Test { + struct FakeDispute { + bytes32 requestId; + bytes32 test; + } + + struct FakeRequest { + address disputeModule; + } + + // The target contract + ForTest_BondEscalationResolutionModule public module; + + // A mock oracle + IOracle public oracle; + + // A mock accounting extension + IBondEscalationAccounting public accounting; + + // A mock token + IERC20 public token; + + // Mock EOA proposer + address public proposer; + + // Mock EOA disputer + address public disputer; + + // Mock EOA pledgerFor + address public pledgerFor; + + // Mock EOA pledgerAgainst + address public pledgerAgainst; + + // Mock percentageDiff + uint256 percentageDiff; + + // Mock pledge threshold + uint256 pledgeThreshold; + + // Mock time until main deadline + uint256 timeUntilDeadline; + + // Mock time to break inequality + uint256 timeToBreakInequality; + + /** + * @notice Deploy the target and mock oracle+accounting extension + */ + function setUp() public { + oracle = IOracle(makeAddr('Oracle')); + vm.etch(address(oracle), hex'069420'); + + accounting = IBondEscalationAccounting(makeAddr('AccountingExtension')); + vm.etch(address(accounting), hex'069420'); + + token = IERC20(makeAddr('ERC20')); + vm.etch(address(token), hex'069420'); + + proposer = makeAddr('proposer'); + disputer = makeAddr('disputer'); + pledgerFor = makeAddr('pledgerFor'); + pledgerAgainst = makeAddr('pledgerAgainst'); + + // Avoid starting at 0 for time sensitive tests + vm.warp(123_456); + + module = new ForTest_BondEscalationResolutionModule(oracle); + } + + //////////////////////////////////////////////////////////////////// + // Tests for moduleName + //////////////////////////////////////////////////////////////////// + + /** + * @notice Test that the moduleName function returns the correct name + */ + function test_moduleName() public { + assertEq(module.moduleName(), 'BondEscalationResolutionModule'); + } + + //////////////////////////////////////////////////////////////////// + // Tests for escalateDispute + //////////////////////////////////////////////////////////////////// + + /* + Specs: + 0. Should do the appropiate calls and emit the appropiate events + 1. Should revert if caller is not the dispute module + 2. Should set escalationData.startTime to block.timestamp + */ + + function test_escalateDispute(bytes32 _disputeId, bytes32 _requestId, address _disputeModule) public { + IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId); + IOracle.Request memory _mockRequest = _getMockRequest(_disputeModule); + + vm.mockCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId))); + + vm.mockCall(address(oracle), abi.encodeCall(IOracle.getRequest, (_requestId)), abi.encode(_mockRequest)); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.getRequest, (_requestId))); + + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_OnlyDisputeModule.selector); + module.escalateDispute(_disputeId); + + vm.prank(_disputeModule); + module.escalateDispute(_disputeId); + + (, uint128 _startTime,,) = module.escalationData(_disputeId); + + assertEq(_startTime, uint128(block.timestamp)); + + // TODO: expect event emission + } + + //////////////////////////////////////////////////////////////////// + // Tests for pledgeForDispute + //////////////////////////////////////////////////////////////////// + + /* + Specs: + 0. Should do the appropiate calls, updates, and emit the appropiate events + 1. Should revert if dispute is not escalated (_startTime == 0) -> done + 2. Should revert if we are in or past the deadline -> done + 3. Should revert if the pledging phase is over and settlement is required -> done + 4. Should not allow pledging if inequality status is set to AgainstTurnToVote -> done + 5. Should be able to pledge if min threshold not reached, if status is Equalized or ForTurnToVote + 6. After pledging, if the new amount of pledges has not surpassed the min threshold, it should do an early return and + leave the status as Unstarted + 7. After pledging, if the new amount of pledges surpassed the threshold but not the percentage difference, it should set the + status to Equalized and the timer to 0. + 7. After pledging, if the new pledges surpassed the min threshold and the percentage difference, it should set + the inequality status to AgainstDisputeTurn and the timer to block.timestamp + 8. After pledging, if the againstPledges percentage is still higher than the percentage difference and the status prior was + set to ForDisputeTurn, then leave as ForDisputeTurn without resetting the timer + 9. After pledging, if the againstPercentage votes dropped below the percentage diff and the status was ForDisputeTurn, then + set the inequality status to Equalize and the timer to 0. + + Misc: + - Stress test math + */ + + function test_pledgeForDisputeReverts(bytes32 _requestId, bytes32 _disputeId, uint256 _pledgeAmount) public { + IBondEscalationResolutionModule.Resolution _resolution = IBondEscalationResolutionModule.Resolution.Unresolved; + uint128 _startTime = 0; + uint256 _pledgesFor = 0; + uint256 _pledgesAgainst = 0; + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_NotEscalated.selector); + module.pledgeForDispute(_requestId, _disputeId, _pledgeAmount); + + // Test revert when deadline over + _startTime = 1; + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + uint256 _timeUntilDeadline = block.timestamp - _startTime; + _setRequestData(_requestId, percentageDiff, pledgeThreshold, _timeUntilDeadline, timeToBreakInequality); + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_PledgingPhaseOver.selector); + module.pledgeForDispute(_requestId, _disputeId, _pledgeAmount); + + // Test revert when the dispute must be resolved + uint256 _time = block.timestamp; + + IBondEscalationResolutionModule.InequalityStatus _inequalityStatus = + IBondEscalationResolutionModule.InequalityStatus.AgainstTurnToEqualize; + _setInequalityData(_disputeId, _inequalityStatus, _time); + + _startTime = uint128(block.timestamp); + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + _timeUntilDeadline = 10_000; + uint256 _timeToBreakInequality = 5000; + + _setRequestData(_requestId, percentageDiff, pledgeThreshold, _timeUntilDeadline, _timeToBreakInequality); + + vm.warp(block.timestamp + _timeToBreakInequality); + + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_MustBeResolved.selector); + module.pledgeForDispute(_requestId, _disputeId, _pledgeAmount); + + // Test revert when status == AgainstTurnToEqualize + vm.warp(block.timestamp - _timeToBreakInequality - 1); // Not past the deadline anymore + _setInequalityData(_disputeId, _inequalityStatus, _time); + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_AgainstTurnToEqualize.selector); + module.pledgeForDispute(_requestId, _disputeId, _pledgeAmount); + } + + //////////////////////////////////////////////////////////////////// + // Tests for pledgeAgainstDispute + //////////////////////////////////////////////////////////////////// + + /* + Specs: + 0. Should do the appropiate calls, updates, and emit the appropiate events + 1. Should revert if dispute is not escalated (_startTime == 0) -> done + 2. Should revert if we are in or past the deadline -> done + 3. Should revert if the pledging phase is over and settlement is required -> done + 4. Should not allow pledging if inequality status is set to ForTurnToVote -> done + 5. Should be able to pledge if min threshold not reached, if status is Equalized or AgainstTurnToVote + 6. After pledging, if the new amount of pledges has not surpassed the min threshold, it should do an early return and + leave the status as Unstarted + 7. After pledging, if the new amount of pledges surpassed the threshold but not the percentage difference, it should set the + status to Equalized and the timer to 0. + 7. After pledging, if the new pledges surpassed the min threshold and the percentage difference, it should set + the inequality status to ForDisputeTurn and the timer to block.timestamp + 8. After pledging, if the forPledges percentage is still higher than the percentage difference and the status prior was + set to AgainstDisputeTurn, then leave as AgainstDisputeTurn without resetting the timer + 9. After pledging, if the forPercentage votes dropped below the percentage diff and the status was AgainstDisputeTurn, then + set the inequality status to Equalize and the timer to 0. + + Misc: + - Stress test math + */ + + function test_pledgeAgainstDisputeReverts(bytes32 _requestId, bytes32 _disputeId, uint256 _pledgeAmount) public { + IBondEscalationResolutionModule.Resolution _resolution = IBondEscalationResolutionModule.Resolution.Unresolved; + uint128 _startTime = 0; + uint256 _pledgesFor = 0; + uint256 _pledgesAgainst = 0; + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_NotEscalated.selector); + module.pledgeForDispute(_requestId, _disputeId, _pledgeAmount); + + // Test revert when deadline over + _startTime = 1; + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + uint256 _timeUntilDeadline = block.timestamp - _startTime; + _setRequestData(_requestId, percentageDiff, pledgeThreshold, _timeUntilDeadline, timeToBreakInequality); + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_PledgingPhaseOver.selector); + module.pledgeForDispute(_requestId, _disputeId, _pledgeAmount); + + // Test revert when the dispute must be resolved + uint256 _time = block.timestamp; + + IBondEscalationResolutionModule.InequalityStatus _inequalityStatus = + IBondEscalationResolutionModule.InequalityStatus.AgainstTurnToEqualize; + _setInequalityData(_disputeId, _inequalityStatus, _time); + + _startTime = uint128(block.timestamp); + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + _timeUntilDeadline = 10_000; + uint256 _timeToBreakInequality = 5000; + + _setRequestData(_requestId, percentageDiff, pledgeThreshold, _timeUntilDeadline, _timeToBreakInequality); + + vm.warp(block.timestamp + _timeToBreakInequality); + + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_MustBeResolved.selector); + module.pledgeForDispute(_requestId, _disputeId, _pledgeAmount); + + // Test revert when status == AgainstTurnToEqualize + vm.warp(block.timestamp - _timeToBreakInequality - 1); // Not past the deadline anymore + _setInequalityData(_disputeId, _inequalityStatus, _time); + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_AgainstTurnToEqualize.selector); + module.pledgeForDispute(_requestId, _disputeId, _pledgeAmount); + } + + //////////////////////////////////////////////////////////////////// + // Tests for resolveDispute + //////////////////////////////////////////////////////////////////// + + /* + Specs: + 0. Should revert if the resolution status is different than Unresolved - done + 1. Should revert if the dispute is not escalated (startTime == 0) - done + 2. Should revert if the main deadline has not be reached and the inequality timer has not culminated - done + + 3. After resolve, if the pledges from both sides never reached the threshold, or if the pledges of both sides end up tied + it should set the resolution status to NoResolution. TODO: and do the appropiate calls. + 4. After resolve, if the pledges for the disputer were more than the pledges against him, then it should + set the resolution state to DisputerWon and call the oracle to update the status with Won. Also emit event. + 5. Same as 4 but with DisputerLost, and Lost when the pledges against the disputer were more than the pledges in favor of + the disputer. + */ + + function test_resolveDisputeReverts(bytes32 _requestId, bytes32 _disputeId) public { + // Revert if status is different Unresolved + IBondEscalationResolutionModule.Resolution _resolution = IBondEscalationResolutionModule.Resolution.DisputerWon; + uint128 _startTime = 0; + uint256 _pledgesFor = 0; + uint256 _pledgesAgainst = 0; + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_AlreadyResolved.selector); + module.resolveDispute(_disputeId); + + // Revert if dispute not escalated + _resolution = IBondEscalationResolutionModule.Resolution.Unresolved; + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_NotEscalated.selector); + module.resolveDispute(_disputeId); + + // Revert if we have not yet reached the deadline and the timer has not passed + IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId); + vm.mockCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId))); + + uint256 _timeUntilDeadline = 100_000; + uint256 _timeToBreakInequality = 100_000; + _setRequestData(_requestId, percentageDiff, pledgeThreshold, _timeUntilDeadline, _timeToBreakInequality); + + // Test revert when the dispute must be resolved + uint256 _time = block.timestamp; + IBondEscalationResolutionModule.InequalityStatus _inequalityStatus = + IBondEscalationResolutionModule.InequalityStatus.AgainstTurnToEqualize; + _setInequalityData(_disputeId, _inequalityStatus, _time); + + _startTime = uint128(block.timestamp); + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_PledgingPhaseNotOver.selector); + module.resolveDispute(_disputeId); + } + + function test_resolveDisputeThresholdNotReached(bytes32 _requestId, bytes32 _disputeId) public { + // START OF SETUP TO AVOID REVERTS + + IBondEscalationResolutionModule.Resolution _resolution = IBondEscalationResolutionModule.Resolution.Unresolved; + uint128 _startTime = 1; // not zero + uint256 _pledgesFor = 0; + uint256 _pledgesAgainst = 0; + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId); + vm.mockCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId))); + + // END OF SETUP TO AVOID REVERTS + + // START OF TEST THRESHOLD NOT REACHED + uint256 _pledgeThreshold = 1000; + _setRequestData(_requestId, percentageDiff, _pledgeThreshold, timeUntilDeadline, timeToBreakInequality); + module.resolveDispute(_disputeId); + + (IBondEscalationResolutionModule.Resolution _trueResStatus,,,) = module.escalationData(_disputeId); + + assertEq(uint256(_trueResStatus), uint256(IBondEscalationResolutionModule.Resolution.NoResolution)); + + // END OF TEST THRESHOLD NOT REACHED + } + + function test_resolveDisputeTiedPledges(bytes32 _requestId, bytes32 _disputeId) public { + // START OF SETUP TO AVOID REVERTS + + IBondEscalationResolutionModule.Resolution _resolution = IBondEscalationResolutionModule.Resolution.Unresolved; + uint128 _startTime = 1; // not zero + uint256 _pledgesFor = 2000; + uint256 _pledgesAgainst = 2000; + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId); + vm.mockCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId))); + + // END OF SETUP TO AVOID REVERTS + + // START OF TIED PLEDGES + _setRequestData(_requestId, percentageDiff, pledgeThreshold, timeUntilDeadline, timeToBreakInequality); + module.resolveDispute(_disputeId); + + (IBondEscalationResolutionModule.Resolution _trueResStatus,,,) = module.escalationData(_disputeId); + + assertEq(uint256(_trueResStatus), uint256(IBondEscalationResolutionModule.Resolution.NoResolution)); + + // END OF TIED PLEDGES + } + + function test_resolveDisputeForPledgesWon(bytes32 _requestId, bytes32 _disputeId) public { + // START OF SETUP TO AVOID REVERTS + + IBondEscalationResolutionModule.Resolution _resolution = IBondEscalationResolutionModule.Resolution.Unresolved; + uint128 _startTime = 1; // not zero + uint256 _pledgesFor = 3000; + uint256 _pledgesAgainst = 2000; + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId); + vm.mockCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId))); + + // END OF SETUP TO AVOID REVERTS + + // START OF FOR PLEDGES WON + _setRequestData(_requestId, percentageDiff, pledgeThreshold, timeUntilDeadline, timeToBreakInequality); + + vm.mockCall( + address(oracle), + abi.encodeCall(IOracle.updateDisputeStatus, (_disputeId, IOracle.DisputeStatus.Won)), + abi.encode() + ); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.updateDisputeStatus, (_disputeId, IOracle.DisputeStatus.Won))); + + module.resolveDispute(_disputeId); + + (IBondEscalationResolutionModule.Resolution _trueResStatus,,,) = module.escalationData(_disputeId); + + assertEq(uint256(_trueResStatus), uint256(IBondEscalationResolutionModule.Resolution.DisputerWon)); + + // END OF FOR PLEDGES WON + } + + function test_resolveDisputeAgainstPledgesWon(bytes32 _requestId, bytes32 _disputeId) public { + // START OF SETUP TO AVOID REVERTS + + IBondEscalationResolutionModule.Resolution _resolution = IBondEscalationResolutionModule.Resolution.Unresolved; + uint128 _startTime = 1; // not zero + uint256 _pledgesFor = 2000; + uint256 _pledgesAgainst = 3000; + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + IOracle.Dispute memory _mockDispute = _getMockDispute(_requestId); + vm.mockCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId)), abi.encode(_mockDispute)); + vm.expectCall(address(oracle), abi.encodeCall(IOracle.getDispute, (_disputeId))); + + // END OF SETUP TO AVOID REVERTS + + // START OF FOR PLEDGES LOST + _setRequestData(_requestId, percentageDiff, pledgeThreshold, timeUntilDeadline, timeToBreakInequality); + + vm.mockCall( + address(oracle), + abi.encodeCall(IOracle.updateDisputeStatus, (_disputeId, IOracle.DisputeStatus.Lost)), + abi.encode() + ); + vm.expectCall( + address(oracle), abi.encodeCall(IOracle.updateDisputeStatus, (_disputeId, IOracle.DisputeStatus.Lost)) + ); + + module.resolveDispute(_disputeId); + + (IBondEscalationResolutionModule.Resolution _trueResStatus,,,) = module.escalationData(_disputeId); + + assertEq(uint256(_trueResStatus), uint256(IBondEscalationResolutionModule.Resolution.DisputerLost)); + + // END OF FOR PLEDGES LOST + } + + //////////////////////////////////////////////////////////////////// + // Tests for settleAcountancy + //////////////////////////////////////////////////////////////////// + + /* + Specs: + 0. It should revert if the resolution has not been resolved yet - done + 1. If the disputer won, it should pay those that pledger for the disputer - done but dummy + 2. If the disputerl lost, it should pay those that pledged agains the disputer - done but dummy + 3. If the status is set to NoResolution, it should release the pledges. TODO + + TODO: rigorous math checks -- im using dummy numbers for the current tests + */ + + function test_settleAccountancyRevert(bytes32 _requestId, bytes32 _disputeId) public { + // Revert if status is Unresolved + IBondEscalationResolutionModule.Resolution _resolution = IBondEscalationResolutionModule.Resolution.Unresolved; + uint128 _startTime = 0; + uint256 _pledgesFor = 0; + uint256 _pledgesAgainst = 0; + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + vm.expectRevert(IBondEscalationResolutionModule.BondEscalationResolutionModule_NotResolved.selector); + module.settleAccountancy(_requestId, _disputeId); + } + + function test_settleAccountancyPayForPledgers(bytes32 _requestId, bytes32 _disputeId) public { + // Revert if status is Unresolved + IBondEscalationResolutionModule.Resolution _resolution = IBondEscalationResolutionModule.Resolution.DisputerWon; + + _setRequestData(_requestId, percentageDiff, pledgeThreshold, timeUntilDeadline, timeToBreakInequality); + + uint128 _startTime = 0; + uint256 _pledgesFor = 0; + uint256 _pledgesAgainst = 100_000; + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + uint256 _pledgedAmount = 100; // not important + uint256 _numOfPledgers = 10; + (address[] memory _pledgers, uint256[] memory _amountPledged) = _createPledgers(_numOfPledgers, _pledgedAmount); + + module.forTest_setPledgedFor(_disputeId, _pledgers, _amountPledged); + + // TODO: Round numbers == try with weird numbers in further tests + uint256 _amountPerPledger = _pledgesAgainst / _pledgers.length; + + vm.mockCall( + address(accounting), + abi.encodeCall(accounting.payWinningPledgers, (_requestId, _disputeId, _pledgers, token, _amountPerPledger)), + abi.encode(true) + ); + vm.expectCall( + address(accounting), + abi.encodeCall(accounting.payWinningPledgers, (_requestId, _disputeId, _pledgers, token, _amountPerPledger)) + ); + + module.settleAccountancy(_requestId, _disputeId); + } + + function test_settleAccountancyPayAgainstPledgers(bytes32 _requestId, bytes32 _disputeId) public { + // Revert if status is Unresolved + IBondEscalationResolutionModule.Resolution _resolution = IBondEscalationResolutionModule.Resolution.DisputerLost; + + _setRequestData(_requestId, percentageDiff, pledgeThreshold, timeUntilDeadline, timeToBreakInequality); + + uint128 _startTime = 0; + uint256 _pledgesFor = 100_000; + uint256 _pledgesAgainst = 0; + _setMockEscalationData(_disputeId, _resolution, _startTime, _pledgesFor, _pledgesAgainst); + + uint256 _pledgedAmount = 100; // not important + uint256 _numOfPledgers = 10; + (address[] memory _pledgers, uint256[] memory _amountPledged) = _createPledgers(_numOfPledgers, _pledgedAmount); + + module.forTest_setPledgedAgainst(_disputeId, _pledgers, _amountPledged); + + // TODO: Round numbers == try with weird numbers in further tests + uint256 _amountPerPledger = _pledgesFor / _pledgers.length; + + vm.mockCall( + address(accounting), + abi.encodeCall(accounting.payWinningPledgers, (_requestId, _disputeId, _pledgers, token, _amountPerPledger)), + abi.encode(true) + ); + vm.expectCall( + address(accounting), + abi.encodeCall(accounting.payWinningPledgers, (_requestId, _disputeId, _pledgers, token, _amountPerPledger)) + ); + + module.settleAccountancy(_requestId, _disputeId); + } + + //////////////////////////////////////////////////////////////////// + // Tests for fetchPledgeDataFor + //////////////////////////////////////////////////////////////////// + + /* + Specs: + 0. It should fetch those that pledges for the disputer + */ + + function test_fetchPledgeDataFor(bytes32 _disputeId, uint256 _pledgedAmount) public { + uint256 _numOfPledgers = 10; + (address[] memory _pledgers, uint256[] memory _amountPledged) = _createPledgers(_numOfPledgers, _pledgedAmount); + + module.forTest_setPledgedFor(_disputeId, _pledgers, _amountPledged); + + IBondEscalationResolutionModule.PledgeData[] memory _pledgeData = module.fetchPledgeDataFor(_disputeId); + + for (uint256 i; i < _pledgeData.length; i++) { + assertEq(_pledgeData[i].pledger, _pledgers[i]); + assertEq(_pledgeData[i].pledges, _amountPledged[i]); + } + } + + //////////////////////////////////////////////////////////////////// + // Tests for fetchPledgeDataAgainst + //////////////////////////////////////////////////////////////////// + + /* + Specs: + 0. It should fetch those that pledges against the disputer + */ + + function test_fetchPledgeDataAgainst(bytes32 _disputeId, uint256 _pledgedAmount) public { + uint256 _numOfPledgers = 10; + (address[] memory _pledgers, uint256[] memory _amountPledged) = _createPledgers(_numOfPledgers, _pledgedAmount); + + module.forTest_setPledgedAgainst(_disputeId, _pledgers, _amountPledged); + + IBondEscalationResolutionModule.PledgeData[] memory _pledgeData = module.fetchPledgeDataAgainst(_disputeId); + + for (uint256 i; i < _pledgeData.length; i++) { + assertEq(_pledgeData[i].pledger, _pledgers[i]); + assertEq(_pledgeData[i].pledges, _amountPledged[i]); + } + } + + //////////////////////////////////////////////////////////////////// + // Tests for decodeRequestData + //////////////////////////////////////////////////////////////////// + + /* + Specs: + 0. It should decode the data correctly + */ + + function test_decodeRequestDataReturnTheCorrectData( + bytes32 _requestId, + uint256 _percentageDiff, + uint256 _pledgeThreshold, + uint256 _timeUntilDeadline, + uint256 _timeToBreakInequality + ) public { + _setRequestData(_requestId, _percentageDiff, _pledgeThreshold, _timeUntilDeadline, _timeToBreakInequality); + ( + IBondEscalationAccounting _accounting, + IERC20 _token, + uint256 __percentageDiff, + uint256 __pledgeThreshold, + uint256 __timeUntilDeadline, + uint256 __timeToBreakInequality + ) = module.decodeRequestData(_requestId); + assertEq(address(accounting), address(_accounting)); + assertEq(address(token), address(_token)); + assertEq(_percentageDiff, __percentageDiff); + assertEq(_pledgeThreshold, __pledgeThreshold); + assertEq(_timeUntilDeadline, __timeUntilDeadline); + assertEq(_timeToBreakInequality, __timeToBreakInequality); + } + + //////////////////////////////////////////////////////////////////// + // Utils + //////////////////////////////////////////////////////////////////// + function _setRequestData( + bytes32 _requestId, + uint256 _percentageDiff, + uint256 _pledgeThreshold, + uint256 _timeUntilDeadline, + uint256 _timeToBreakInequality + ) internal { + bytes memory _data = + abi.encode(accounting, token, _percentageDiff, _pledgeThreshold, _timeUntilDeadline, _timeToBreakInequality); + module.forTest_setRequestData(_requestId, _data); + } + + function _setInequalityData( + bytes32 _disputeId, + IBondEscalationResolutionModule.InequalityStatus _inequalityStatus, + uint256 _time + ) internal { + BondEscalationResolutionModule.InequalityData memory _inequalityData = + IBondEscalationResolutionModule.InequalityData(_inequalityStatus, _time); + module.forTest_setInequalityData(_disputeId, _inequalityData); + } + + function _setMockEscalationData( + bytes32 _disputeId, + IBondEscalationResolutionModule.Resolution _resolution, + uint128 _startTime, + uint256 _pledgesFor, + uint256 _pledgesAgainst + ) internal { + BondEscalationResolutionModule.EscalationData memory _escalationData = + IBondEscalationResolutionModule.EscalationData(_resolution, _startTime, _pledgesFor, _pledgesAgainst); + module.forTest_setEscalationData(_disputeId, _escalationData); + } + + function _getMockDispute(bytes32 _requestId) internal view returns (IOracle.Dispute memory _dispute) { + _dispute = IOracle.Dispute({ + disputer: disputer, + responseId: bytes32('response'), + proposer: proposer, + requestId: _requestId, + status: IOracle.DisputeStatus.Active, + createdAt: block.timestamp + }); + } + + function _getMockRequest(address _disputeModule) internal pure returns (IOracle.Request memory _request) { + _request = IOracle.Request({ + requestModuleData: abi.encode(0), + responseModuleData: abi.encode(0), + disputeModuleData: abi.encode(0), + resolutionModuleData: abi.encode(0), + finalityModuleData: abi.encode(0), + ipfsHash: 0, + requestModule: IRequestModule(address(100)), + responseModule: IResponseModule(address(200)), + disputeModule: IDisputeModule(_disputeModule), + resolutionModule: IResolutionModule(address(400)), + finalityModule: IFinalityModule(address(500)), + requester: address(600), + nonce: 0, + createdAt: 0 + }); + } + + function _createPledgers( + uint256 _numOfPledgers, + uint256 _amount + ) internal returns (address[] memory _pledgers, uint256[] memory _pledgedAmounts) { + _pledgers = new address[](_numOfPledgers); + _pledgedAmounts = new uint256[](_numOfPledgers); + address _pledger; + uint256 _pledge; + + for (uint256 i; i < _numOfPledgers; i++) { + _pledger = makeAddr(string.concat('pledger', Strings.toString(i))); + _pledgers[i] = _pledger; + } + + for (uint256 j; j < _numOfPledgers; j++) { + _pledge = _amount / (j + 100); + _pledgedAmounts[j] = _pledge; + } + + return (_pledgers, _pledgedAmounts); + } +}