From ec62b96cbf9995d75d5ed4880b62ae540d113715 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:34:09 +0400 Subject: [PATCH 1/8] feat: update `prophet-core` package --- package.json | 2 +- yarn.lock | 72 +++++++++++++++++++++++++--------------------------- 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index b7700bc1..e85440a1 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "package.json": "sort-package-json" }, "dependencies": { - "@defi-wonderland/prophet-core-contracts": "0.0.0-d05a00d0", + "@defi-wonderland/prophet-core-contracts": "0.0.0-48b0248d", "@defi-wonderland/solidity-utils": "0.0.0-3e9c8e8b", "@openzeppelin/contracts": "^4.9.3", "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", diff --git a/yarn.lock b/yarn.lock index c61b7709..23515a4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -192,10 +192,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@defi-wonderland/prophet-core-contracts@0.0.0-d05a00d0": - version "0.0.0-d05a00d0" - resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-d05a00d0.tgz#1357d917fe46a5a12faa67f557e990255dda14fd" - integrity sha512-F/y0r/qDLFACzsN7Y2VRAPIS9Yhx2btU/m7cQT7T84TbIxAmBGVw6/7nb+HeIbXh+QDO90RP6vHAdQOow/q1Xw== +"@defi-wonderland/prophet-core-contracts@0.0.0-48b0248d": + version "0.0.0-48b0248d" + resolved "https://registry.yarnpkg.com/@defi-wonderland/prophet-core-contracts/-/prophet-core-contracts-0.0.0-48b0248d.tgz#16d0473360074f17b66199c8e3660b71a3d72ad4" + integrity sha512-bEufdaPkLcg1VuYpTWRB5Xf4pmpV3wi0487taGI4A+YtwhMsIh9ZCNPdgWssqLvIfMPlB4FdGOM936l1yqAKYQ== dependencies: "@defi-wonderland/solidity-utils" "0.0.0-3e9c8e8b" "@openzeppelin/contracts" "^4.9.3" @@ -324,9 +324,9 @@ ts-essentials "^7.0.1" "@types/minimist@^1.2.0": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.4.tgz#81f886786411c45bba3f33e781ab48bd56bfca2e" - integrity sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" + integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== "@types/node@20.5.1": version "20.5.1" @@ -334,9 +334,9 @@ integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== "@types/normalize-package-data@^2.4.0": - version "2.4.3" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz#291c243e4b94dbfbc0c0ee26b7666f1d5c030e2c" - integrity sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg== + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/prettier@^2.1.1": version "2.7.3" @@ -357,9 +357,9 @@ acorn-jsx@^5.0.0: integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + version "8.3.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" + integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== acorn@^6.0.7: version "6.4.2" @@ -367,9 +367,9 @@ acorn@^6.0.7: integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== acorn@^8.4.1: - version "8.10.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" - integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== add-stream@^1.0.0: version "1.0.0" @@ -1181,14 +1181,10 @@ dotgitignore@^2.1.0: find-up "^3.0.0" minimatch "^3.0.4" -"ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": - version "1.0.0" - resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" - -"ds-test@https://github.com/dapphub/ds-test": +"ds-test@git+https://github.com/dapphub/ds-test.git", "ds-test@git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" uid e282159d5170298eb2455a6c05280ab5a73a4ef0 - resolved "https://github.com/dapphub/ds-test#e282159d5170298eb2455a6c05280ab5a73a4ef0" + resolved "git+https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0" "ds-test@https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0": version "1.0.0" @@ -1457,9 +1453,9 @@ fast-diff@^1.1.2, fast-diff@^1.2.0: integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== fast-glob@^3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -1570,14 +1566,14 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +"forge-std@git+https://github.com/foundry-rs/forge-std.git": + version "1.7.1" + resolved "git+https://github.com/foundry-rs/forge-std.git#37a37ab73364d6644bfe11edf88a07880f99bd56" + "forge-std@git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e": version "1.5.6" resolved "git+https://github.com/foundry-rs/forge-std.git#e8a047e3f40f13fa37af6fe14e6e06283d9a060e" -"forge-std@https://github.com/foundry-rs/forge-std": - version "1.7.1" - resolved "https://github.com/foundry-rs/forge-std#267acd30a625086b3f16e1a28cfe0c5097fa46b8" - "forge-std@https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421": version "1.7.1" resolved "https://github.com/foundry-rs/forge-std.git#f73c73d2018eb6a111f35e4dae7b4f27401e9421" @@ -2858,9 +2854,9 @@ progress@^2.0.0: integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== q@^1.5.1: version "1.5.1" @@ -3787,9 +3783,9 @@ universalify@^0.1.0: integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== uri-js@^4.2.2: version "4.4.1" @@ -3916,9 +3912,9 @@ yallist@^4.0.0: integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^2.2.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.3.tgz#01f6d18ef036446340007db8e016810e5d64aad9" - integrity sha512-zw0VAJxgeZ6+++/su5AFoqBbZbrEakwu+X0M5HmcwUiBL7AzcuPKjj5we4xfQLp78LkEMpD0cOnUhmgOVy3KdQ== + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" From 3caf37c5d2aca4cedb4b4fddc80437c8b55a0e94 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:34:39 +0400 Subject: [PATCH 2/8] perf: optimize `ERC20ResolutionModule` --- .../resolution/erc20_resolution_module.md | 2 +- .../resolution/ERC20ResolutionModule.sol | 237 ++--- .../resolution/IERC20ResolutionModule.sol | 297 ++++--- .../resolution/ERC20ResolutionModule.t.sol | 809 +++++++++--------- 4 files changed, 681 insertions(+), 664 deletions(-) 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)); + } + } +} From 761589ba4dd3370f95c7f981dc84ed153429d100 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:38:29 +0400 Subject: [PATCH 3/8] feat: uncomment helpers --- .../extensions/IAccountingExtension.sol | 380 +++++++++--------- solidity/test/utils/Helpers.sol | 122 +++--- 2 files changed, 256 insertions(+), 246 deletions(-) diff --git a/solidity/interfaces/extensions/IAccountingExtension.sol b/solidity/interfaces/extensions/IAccountingExtension.sol index 5a42243e..74c9c9cd 100644 --- a/solidity/interfaces/extensions/IAccountingExtension.sol +++ b/solidity/interfaces/extensions/IAccountingExtension.sol @@ -1,190 +1,190 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; - -// /* -// * @title AccountingExtension -// * @notice Extension allowing users to deposit and bond funds -// * to be used for payments and disputes. -// */ -// interface IAccountingExtension { -// /*/////////////////////////////////////////////////////////////// -// EVENTS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice A user deposited tokens into the accounting extension -// * @param _depositor The user who deposited the tokens -// * @param _token The address of the token deposited by the user -// * @param _amount The amount of `_token` deposited -// */ -// event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A user withdrew tokens from the accounting extension -// * @param _withdrawer The user who withdrew the tokens -// * @param _token The address of the token withdrawn by the user -// * @param _amount The amount of `_token` withdrawn -// */ -// event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice A payment between users has been made -// * @param _beneficiary The user receiving the tokens -// * @param _payer The user who is getting its tokens transferred -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` transferred -// */ -// event Paid( -// bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount -// ); - -// /** -// * @notice User's funds have been bonded -// * @param _bonder The user who is getting its tokens bonded -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` bonded -// */ -// event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); - -// /** -// * @notice User's funds have been released -// * @param _beneficiary The user who is getting its tokens released -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` released -// */ -// event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// ERRORS -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Thrown when the account doesn't have enough balance to bond/withdraw -// * or not enough bonded to release/pay -// */ -// error AccountingExtension_InsufficientFunds(); - -// /** -// * @notice Thrown when the module bonding user tokens hasn't been approved by the user. -// */ -// error AccountingExtension_InsufficientAllowance(); - -// /** -// * @notice Thrown when an `onlyAllowedModule` function is called by something -// * else than a module being used in the corresponding request -// */ -// error AccountingExtension_UnauthorizedModule(); - -// /** -// * @notice Thrown when an `onlyParticipant` function is called with an address -// * that is not part of the request. -// */ -// error AccountingExtension_UnauthorizedUser(); - -// /*/////////////////////////////////////////////////////////////// -// VARIABLES -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Returns the interface for the Oracle contract -// */ -// function ORACLE() external view returns (IOracle _oracle); - -// /** -// * @notice Returns the amount of a token a user has bonded -// * @param _user The address of the user with bonded tokens -// * @param _bondToken The token bonded -// * @param _requestId The id of the request the user bonded for -// * @return _amount The amount of `_bondToken` bonded -// */ -// function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); - -// /** -// * @notice Returns the amount of a token a user has deposited -// * @param _user The address of the user with deposited tokens -// * @param _token The token deposited -// * @return _amount The amount of `_token` deposited -// */ -// function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); - -// /*/////////////////////////////////////////////////////////////// -// LOGIC -// //////////////////////////////////////////////////////////////*/ - -// /** -// * @notice Transfers tokens from a user and updates his virtual balance -// * @dev The user must have approved the accounting extension to transfer the tokens. -// * @param _token The address of the token being deposited -// * @param _amount The amount of `_token` to deposit -// */ -// function deposit(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows an user to withdraw deposited tokens -// * @param _token The address of the token being withdrawn -// * @param _amount The amount of `_token` to withdraw -// */ -// function withdraw(IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to transfer bonded tokens from one user to another -// * @dev Only the virtual balances in the accounting extension are modified. The token contract -// * is not called nor its balances modified. -// * @param _requestId The id of the request handling the user's tokens -// * @param _payer The address of the user paying the tokens -// * @param _receiver The address of the user receiving the tokens -// * @param _token The address of the token being transferred -// * @param _amount The amount of `_token` being transferred -// */ -// function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a allowed module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a valid module to bond a user's tokens for a request -// * @param _bonder The address of the user to bond tokens for -// * @param _requestId The id of the request the user is bonding for -// * @param _token The address of the token being bonded -// * @param _amount The amount of `_token` to bond -// * @param _sender The address starting the propose call on the Oracle -// */ -// function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; - -// /** -// * @notice Allows a valid module to release a user's tokens -// * @param _bonder The address of the user to release tokens for -// * @param _requestId The id of the request where the tokens were bonded -// * @param _token The address of the token being released -// * @param _amount The amount of `_token` to release -// */ -// function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; - -// /** -// * @notice Allows a user to approve a module for bonding tokens -// * @param _module The address of the module to be approved -// */ -// function approveModule(address _module) external; - -// /** -// * @notice Allows a user to revoke a module's approval for bonding tokens -// * @param _module The address of the module to be revoked -// */ -// function revokeModule(address _module) external; - -// /** -// * @notice Returns a list of all modules a user has approved -// * @param _user The address of the user -// * @return _approvedModules The array of all modules approved by the user -// */ -// function approvedModules(address _user) external view returns (address[] memory _approvedModules); -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; + +/* + * @title AccountingExtension + * @notice Extension allowing users to deposit and bond funds + * to be used for payments and disputes. + */ +interface IAccountingExtension { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice A user deposited tokens into the accounting extension + * @param _depositor The user who deposited the tokens + * @param _token The address of the token deposited by the user + * @param _amount The amount of `_token` deposited + */ + event Deposited(address indexed _depositor, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A user withdrew tokens from the accounting extension + * @param _withdrawer The user who withdrew the tokens + * @param _token The address of the token withdrawn by the user + * @param _amount The amount of `_token` withdrawn + */ + event Withdrew(address indexed _withdrawer, IERC20 indexed _token, uint256 _amount); + + /** + * @notice A payment between users has been made + * @param _beneficiary The user receiving the tokens + * @param _payer The user who is getting its tokens transferred + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` transferred + */ + event Paid( + bytes32 indexed _requestId, address indexed _beneficiary, address indexed _payer, IERC20 _token, uint256 _amount + ); + + /** + * @notice User's funds have been bonded + * @param _bonder The user who is getting its tokens bonded + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` bonded + */ + event Bonded(bytes32 indexed _requestId, address indexed _bonder, IERC20 indexed _token, uint256 _amount); + + /** + * @notice User's funds have been released + * @param _beneficiary The user who is getting its tokens released + * @param _token The address of the token being released + * @param _amount The amount of `_token` released + */ + event Released(bytes32 indexed _requestId, address indexed _beneficiary, IERC20 indexed _token, uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Thrown when the account doesn't have enough balance to bond/withdraw + * or not enough bonded to release/pay + */ + error AccountingExtension_InsufficientFunds(); + + /** + * @notice Thrown when the module bonding user tokens hasn't been approved by the user. + */ + error AccountingExtension_InsufficientAllowance(); + + /** + * @notice Thrown when an `onlyAllowedModule` function is called by something + * else than a module being used in the corresponding request + */ + error AccountingExtension_UnauthorizedModule(); + + /** + * @notice Thrown when an `onlyParticipant` function is called with an address + * that is not part of the request. + */ + error AccountingExtension_UnauthorizedUser(); + + /*/////////////////////////////////////////////////////////////// + VARIABLES + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Returns the interface for the Oracle contract + */ + function ORACLE() external view returns (IOracle _oracle); + + /** + * @notice Returns the amount of a token a user has bonded + * @param _user The address of the user with bonded tokens + * @param _bondToken The token bonded + * @param _requestId The id of the request the user bonded for + * @return _amount The amount of `_bondToken` bonded + */ + function bondedAmountOf(address _user, IERC20 _bondToken, bytes32 _requestId) external returns (uint256 _amount); + + /** + * @notice Returns the amount of a token a user has deposited + * @param _user The address of the user with deposited tokens + * @param _token The token deposited + * @return _amount The amount of `_token` deposited + */ + function balanceOf(address _user, IERC20 _token) external view returns (uint256 _amount); + + /*/////////////////////////////////////////////////////////////// + LOGIC + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Transfers tokens from a user and updates his virtual balance + * @dev The user must have approved the accounting extension to transfer the tokens. + * @param _token The address of the token being deposited + * @param _amount The amount of `_token` to deposit + */ + function deposit(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows an user to withdraw deposited tokens + * @param _token The address of the token being withdrawn + * @param _amount The amount of `_token` to withdraw + */ + function withdraw(IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to transfer bonded tokens from one user to another + * @dev Only the virtual balances in the accounting extension are modified. The token contract + * is not called nor its balances modified. + * @param _requestId The id of the request handling the user's tokens + * @param _payer The address of the user paying the tokens + * @param _receiver The address of the user receiving the tokens + * @param _token The address of the token being transferred + * @param _amount The amount of `_token` being transferred + */ + function pay(bytes32 _requestId, address _payer, address _receiver, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a allowed module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a valid module to bond a user's tokens for a request + * @param _bonder The address of the user to bond tokens for + * @param _requestId The id of the request the user is bonding for + * @param _token The address of the token being bonded + * @param _amount The amount of `_token` to bond + * @param _sender The address starting the propose call on the Oracle + */ + function bond(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount, address _sender) external; + + /** + * @notice Allows a valid module to release a user's tokens + * @param _bonder The address of the user to release tokens for + * @param _requestId The id of the request where the tokens were bonded + * @param _token The address of the token being released + * @param _amount The amount of `_token` to release + */ + function release(address _bonder, bytes32 _requestId, IERC20 _token, uint256 _amount) external; + + /** + * @notice Allows a user to approve a module for bonding tokens + * @param _module The address of the module to be approved + */ + function approveModule(address _module) external; + + /** + * @notice Allows a user to revoke a module's approval for bonding tokens + * @param _module The address of the module to be revoked + */ + function revokeModule(address _module) external; + + /** + * @notice Returns a list of all modules a user has approved + * @param _user The address of the user + * @return _approvedModules The array of all modules approved by the user + */ + function approvedModules(address _user) external view returns (address[] memory _approvedModules); +} diff --git a/solidity/test/utils/Helpers.sol b/solidity/test/utils/Helpers.sol index 46874489..af32e439 100644 --- a/solidity/test/utils/Helpers.sol +++ b/solidity/test/utils/Helpers.sol @@ -1,56 +1,66 @@ -// // SPDX-License-Identifier: MIT -// pragma solidity ^0.8.19; - -// import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -// import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; -// import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; - -// import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; - -// contract Helpers is DSTestPlus { -// modifier assumeFuzzable(address _address) { -// _assumeFuzzable(_address); -// _; -// } - -// function _assumeFuzzable(address _address) internal pure { -// assumeNotForgeAddress(_address); -// assumeNotZeroAddress(_address); -// assumeNotPrecompile(_address); -// } - -// function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { -// vm.mockCall(_receiver, _calldata, _returned); -// vm.expectCall(_receiver, _calldata); -// } - -// function _getMockDispute( -// bytes32 _requestId, -// address _disputer, -// address _proposer -// ) internal view returns (IOracle.Dispute memory _dispute) { -// _dispute = IOracle.Dispute({ -// disputer: _disputer, -// responseId: bytes32('response'), -// proposer: _proposer, -// requestId: _requestId, -// status: IOracle.DisputeStatus.None, -// createdAt: block.timestamp -// }); -// } - -// function _forBondDepositERC20( -// IAccountingExtension _accountingExtension, -// address _depositor, -// IERC20 _token, -// uint256 _depositAmount, -// uint256 _balanceIncrease -// ) internal { -// vm.assume(_balanceIncrease >= _depositAmount); -// deal(address(_token), _depositor, _balanceIncrease); -// vm.startPrank(_depositor); -// _token.approve(address(_accountingExtension), _depositAmount); -// _accountingExtension.deposit(_token, _depositAmount); -// vm.stopPrank(); -// } -// } +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {DSTestPlus} from '@defi-wonderland/solidity-utils/solidity/test/DSTestPlus.sol'; +import {IOracle} from '@defi-wonderland/prophet-core-contracts/solidity/interfaces/IOracle.sol'; + +import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol'; + +contract Helpers is DSTestPlus { + modifier assumeFuzzable(address _address) { + _assumeFuzzable(_address); + _; + } + + function _assumeFuzzable(address _address) internal pure { + assumeNotForgeAddress(_address); + assumeNotZeroAddress(_address); + assumeNotPrecompile(_address); + } + + function _mockAndExpect(address _receiver, bytes memory _calldata, bytes memory _returned) internal { + vm.mockCall(_receiver, _calldata, _returned); + vm.expectCall(_receiver, _calldata); + } + + function _getMockDispute( + bytes32 _requestId, + address _disputer, + address _proposer + ) internal view returns (IOracle.Dispute memory _dispute) { + _dispute = IOracle.Dispute({ + disputer: _disputer, + responseId: bytes32('response'), + proposer: _proposer, + requestId: _requestId + }); + } + + function _forBondDepositERC20( + IAccountingExtension _accountingExtension, + address _depositor, + IERC20 _token, + uint256 _depositAmount, + uint256 _balanceIncrease + ) internal { + vm.assume(_balanceIncrease >= _depositAmount); + deal(address(_token), _depositor, _balanceIncrease); + vm.startPrank(_depositor); + _token.approve(address(_accountingExtension), _depositAmount); + _accountingExtension.deposit(_token, _depositAmount); + vm.stopPrank(); + } + + function _getId(IOracle.Response memory _response) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_response)); + } + + function _getId(IOracle.Request memory _request) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_request)); + } + + function _getId(IOracle.Dispute memory _dispute) internal pure returns (bytes32 _id) { + _id = keccak256(abi.encode(_dispute)); + } +} From 580f9cabb909cfde1172c3a1013b213ae76ffe03 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sat, 11 Nov 2023 19:33:11 +0400 Subject: [PATCH 4/8] test: index disputeId to fix a unit test --- solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol b/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol index 5cc4878a..2af65544 100644 --- a/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol +++ b/solidity/test/unit/modules/dispute/CircuitResolverModule.t.sol @@ -40,7 +40,7 @@ contract BaseTest is Test, Helpers { MockVerifier public mockVerifier; // Events - event DisputeStatusChanged(bytes32 _disputeId, IOracle.Dispute _dispute, IOracle.DisputeStatus _status); + event DisputeStatusChanged(bytes32 indexed _disputeId, IOracle.Dispute _dispute, IOracle.DisputeStatus _status); event ResponseDisputed( bytes32 indexed _requestId, bytes32 indexed _responseId, From d6daaec820681fe0d7ae48cf4b388c7a9a5dcbce Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sun, 12 Nov 2023 00:55:26 +0400 Subject: [PATCH 5/8] docs: revise natspec and documentation --- .../resolution/erc20_resolution_module.md | 10 ++--- .../resolution/ERC20ResolutionModule.sol | 5 ++- .../resolution/IERC20ResolutionModule.sol | 45 ++++++++++++++----- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/docs/src/content/modules/resolution/erc20_resolution_module.md b/docs/src/content/modules/resolution/erc20_resolution_module.md index 3905c8fb..6be220b3 100644 --- a/docs/src/content/modules/resolution/erc20_resolution_module.md +++ b/docs/src/content/modules/resolution/erc20_resolution_module.md @@ -10,11 +10,11 @@ The `ERC20ResolutionModule` is a dispute resolution module that decides on the o ### Key Methods -- `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. -- `getVoters(bytes32 _disputeId)`: Returns the addresses of the voters for a given dispute. +- `decodeRequestData`: Decodes the request data associated with a given request ID. +- `startResolution`: Starts the resolution process for a given dispute. +- `castVote`: Allows a user to cast votes for a dispute. +- `resolveDispute`: Resolves a dispute based on the votes cast. +- `getVoters`: Returns the addresses of the voters for a given dispute. ### Request Parameters diff --git a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol index dfd9197b..6ffa6791 100644 --- a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol +++ b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol @@ -22,7 +22,10 @@ contract ERC20ResolutionModule is Module, IERC20ResolutionModule { /// @inheritdoc IERC20ResolutionModule mapping(bytes32 _disputeId => mapping(address _voter => uint256 _numOfVotes)) public votes; - mapping(bytes32 _disputeId => EnumerableSet.AddressSet _votersSet) private _voters; + /** + * @notice The list of voters for each dispute + */ + mapping(bytes32 _disputeId => EnumerableSet.AddressSet _votersSet) internal _voters; constructor(IOracle _oracle) Module(_oracle) {} diff --git a/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol b/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol index 58cb623b..38995b4c 100644 --- a/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol +++ b/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol @@ -105,22 +105,36 @@ interface IERC20ResolutionModule is IResolutionModule { /** * @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 + * + * @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); + /** + * @notice Returns the amount of votes the given voter has cast for the given dispute + * + * @param _disputeId The id of the dispute + * @param _voter The address of the voter + * @return _votes The amount of votes the voter has cast + */ 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 + * + * @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 + /** + * @notice Casts a vote in favor of a dispute + * + * @param _request The request for which the dispute was created + * @param _dispute The dispute for which the vote is being cast + */ function startResolution( bytes32 _disputeId, IOracle.Request calldata _request, @@ -130,7 +144,10 @@ interface IERC20ResolutionModule is IResolutionModule { /** * @notice Casts a vote in favor of a dispute - * @param _numberOfVotes The number of votes to cast + * + * @param _request The request for which the dispute was created + * @param _dispute The dispute for which the vote is being cast + * @param _numberOfVotes The number of votes to cast */ function castVote( IOracle.Request calldata _request, @@ -138,7 +155,14 @@ interface IERC20ResolutionModule is IResolutionModule { uint256 _numberOfVotes ) external; - /// @inheritdoc IResolutionModule + /** + * @notice Settles the dispute, transferring the funds back to the voters + * + * @param _disputeId The id of the dispute + * @param _request The request for which the dispute was created + * @param _response The disputed response + * @param _dispute The dispute that is being resolved + */ function resolveDispute( bytes32 _disputeId, IOracle.Request calldata _request, @@ -148,8 +172,9 @@ interface IERC20ResolutionModule is IResolutionModule { /** * @notice Gets the voters of a dispute - * @param _disputeId The id of the dispute - * @return _voters The addresses of the voters + * + * @param _disputeId The id of the dispute + * @return _voters The addresses of the voters */ function getVoters(bytes32 _disputeId) external view returns (address[] memory _voters); } From 02a871bfc2846e31769012cf031fae013f90a842 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sun, 12 Nov 2023 00:57:19 +0400 Subject: [PATCH 6/8] feat: simplify checks, reducing external calls --- .../modules/resolution/ERC20ResolutionModule.sol | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol index 6ffa6791..ae1cf75e 100644 --- a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol +++ b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol @@ -57,11 +57,11 @@ contract ERC20ResolutionModule is Module, IERC20ResolutionModule { 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(); + if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.Escalated) { + revert ERC20ResolutionModule_AlreadyResolved(); + } RequestParameters memory _params = decodeRequestData(_request.resolutionModuleData); uint256 _deadline = _escalation.startTime + _params.timeUntilDeadline; @@ -84,11 +84,12 @@ contract ERC20ResolutionModule is Module, IERC20ResolutionModule { 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(); + if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.Escalated) { + revert ERC20ResolutionModule_AlreadyResolved(); + } - Escalation memory _escalation = escalations[_disputeId]; // 1. Check that the dispute is actually escalated + Escalation memory _escalation = escalations[_disputeId]; if (_escalation.startTime == 0) revert ERC20ResolutionModule_DisputeNotEscalated(); // 2. Check that voting deadline is over From 1c39fab5edebcc3ff665aea87a3f121acda69040 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sun, 12 Nov 2023 00:57:39 +0400 Subject: [PATCH 7/8] refactor: remove an unused error --- .../modules/resolution/IERC20ResolutionModule.sol | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol b/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol index 38995b4c..95cbe46f 100644 --- a/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol +++ b/solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol @@ -44,7 +44,7 @@ interface IERC20ResolutionModule is IResolutionModule { error ERC20ResolutionModule_OnlyDisputeModule(); /** - * @notice Throws if the dispute has not been escalated + * @notice Throws if the dispute doesn't exist or has not been escalated */ error ERC20ResolutionModule_DisputeNotEscalated(); @@ -63,11 +63,6 @@ interface IERC20ResolutionModule is IResolutionModule { */ error ERC20ResolutionModule_OnGoingVotingPhase(); - /** - * @notice Throws if the dispute does not exist - */ - error ERC20ResolutionModule_NonExistentDispute(); - /** * @notice Throws if the dispute has already been resolved */ From 0aec25baaaf0310f93801665c3d337853f05d067 Mon Sep 17 00:00:00 2001 From: Gas One Cent <86567384+gas1cent@users.noreply.github.com> Date: Sun, 12 Nov 2023 00:58:35 +0400 Subject: [PATCH 8/8] test: fix unit tests --- .../resolution/ERC20ResolutionModule.t.sol | 284 ++++++++---------- 1 file changed, 131 insertions(+), 153 deletions(-) diff --git a/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol b/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol index dc87ae63..304a55b3 100644 --- a/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol +++ b/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol @@ -5,7 +5,9 @@ import 'forge-std/Test.sol'; import {Helpers} from '../../../utils/Helpers.sol'; +import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.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'; @@ -15,15 +17,26 @@ import { } from '../../../../contracts/modules/resolution/ERC20ResolutionModule.sol'; contract ForTest_ERC20ResolutionModule is ERC20ResolutionModule { + using EnumerableSet for EnumerableSet.AddressSet; + constructor(IOracle _oracle) ERC20ResolutionModule(_oracle) {} - function forTest_setEscalation(bytes32 _disputeId, ERC20ResolutionModule.Escalation calldata __escalation) public { - escalations[_disputeId] = __escalation; + function forTest_setStartTime(bytes32 _disputeId, uint256 _startTime) public { + escalations[_disputeId] = IERC20ResolutionModule.Escalation({ + startTime: _startTime, + totalVotes: 0 // Initial amount of votes + }); } function forTest_setVotes(bytes32 _disputeId, address _voter, uint256 _amountOfVotes) public { votes[_disputeId][_voter] = _amountOfVotes; } + + function forTest_castVote(bytes32 _disputeId, address _voter, uint256 _numberOfVotes) public { + votes[_disputeId][_voter] += _numberOfVotes; + _voters[_disputeId].add(_voter); + escalations[_disputeId].totalVotes += _numberOfVotes; + } } contract BaseTest is Test, Helpers { @@ -33,18 +46,10 @@ contract BaseTest is Test, Helpers { 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 + + uint256 public votingTimeWindow = 40_000; + + // 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); @@ -60,37 +65,15 @@ contract BaseTest is Test, Helpers { 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;) { + function _populateVoters(bytes32 _disputeId, uint256 _amountOfVoters, uint256 _amountOfVotes) internal { + for (uint256 _i = 1; _i <= _amountOfVoters; _i++) { 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; - } + module.forTest_castVote(_disputeId, vm.addr(_i), _amountOfVotes); } } } @@ -107,7 +90,6 @@ contract ERC20ResolutionModule_Unit_ModuleData is BaseTest { * @notice Test that the decodeRequestData function returns the correct values */ function test_decodeRequestData_returnsCorrectData( - bytes32 _requestId, address _token, uint256 _minVotesForQuorum, uint256 _votingTimeWindow @@ -129,56 +111,60 @@ 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 { + function test_revertIfNotOracle(bytes32 _disputeId) 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); + module.startResolution(_disputeId, mockRequest, mockResponse, mockDispute); + } + function test_setsStartTime(bytes32 _disputeId) public { vm.prank(address(oracle)); - module.startResolution(_disputeId, _request, mockResponse, mockDispute); - - (uint256 _startTime,) = module.escalations(_disputeId); + module.startResolution(_disputeId, mockRequest, mockResponse, mockDispute); // Check: `startTime` is set to block.timestamp? + (uint256 _startTime,) = module.escalations(_disputeId); assertEq(_startTime, block.timestamp); } + + function test_emitsEvent(bytes32 _disputeId) public { + // Check: emits VotingPhaseStarted event? + vm.expectEmit(true, true, true, true, address(module)); + emit VotingPhaseStarted(block.timestamp, _disputeId); + + vm.prank(address(oracle)); + module.startResolution(_disputeId, mockRequest, mockResponse, mockDispute); + } } 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); + function test_castVote(uint256 _amountOfVotes, address _voter) public { + uint256 _minVotesForQuorum = 1; - // Store mock escalation data with startTime 100_000 - module.forTest_setEscalation( - _disputeId, - IERC20ResolutionModule.Escalation({ - startTime: 100_000, - totalVotes: 0 // Initial amount of votes + mockRequest.resolutionModuleData = abi.encode( + IERC20ResolutionModule.RequestParameters({ + votingToken: token, + minVotesForQuorum: _minVotesForQuorum, + timeUntilDeadline: votingTimeWindow }) ); + mockDispute.requestId = _getId(mockRequest); + bytes32 _disputeId = _getId(mockDispute); - uint256 _minVotesForQuorum = 1; - uint256 _votingTimeWindow = 40_000; + // Store mock escalation data with startTime 100_000 + module.forTest_setStartTime(_disputeId, 100_000); // Mock and expect IERC20.transferFrom to be called _mockAndExpect( address(token), abi.encodeCall(IERC20.transferFrom, (_voter, address(module), _amountOfVotes)), abi.encode() ); + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Escalated) + ); + // Warp to voting phase vm.warp(130_000); @@ -187,7 +173,7 @@ contract ERC20ResolutionModule_Unit_CastVote is BaseTest { emit VoteCast(_voter, _disputeId, _amountOfVotes); vm.prank(_voter); - module.castVote(_request, mockDispute, _amountOfVotes); + module.castVote(mockRequest, mockDispute, _amountOfVotes); (, uint256 _totalVotes) = module.escalations(_disputeId); // Check: totalVotes is updated? @@ -197,87 +183,75 @@ contract ERC20ResolutionModule_Unit_CastVote is BaseTest { 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 + function test_revertIfNotEscalated(uint256 _numberOfVotes) public { + bytes32 _requestId = _getId(mockRequest); 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); + module.castVote(mockRequest, 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 + function test_revertIfAlreadyResolved(uint256 _amountOfVotes, uint256 _votingTimeWindow) public { + mockRequest.resolutionModuleData = abi.encode( + IERC20ResolutionModule.RequestParameters({ + votingToken: token, + minVotesForQuorum: _amountOfVotes, + timeUntilDeadline: _votingTimeWindow + }) + ); + bytes32 _requestId = _getId(mockRequest); mockDispute.requestId = _requestId; + bytes32 _disputeId = _getId(mockDispute); + + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Won) + ); + + module.forTest_setStartTime(_disputeId, block.timestamp); // Check: reverts if dispute is already resolved? vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_AlreadyResolved.selector); - module.castVote(_request, mockDispute, _amountOfVotes); + module.castVote(mockRequest, 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 { + function test_revertIfVotingPhaseOver(uint256 _numberOfVotes, uint256 _timestamp) public { vm.assume(_timestamp > 140_000); + uint256 _minVotesForQuorum = 1; - // Mock the oracle response for looking up a dispute + mockRequest.resolutionModuleData = abi.encode( + IERC20ResolutionModule.RequestParameters({ + votingToken: token, + minVotesForQuorum: _minVotesForQuorum, + timeUntilDeadline: votingTimeWindow + }) + ); + + bytes32 _requestId = _getId(mockRequest); mockDispute.requestId = _requestId; + bytes32 _disputeId = _getId(mockDispute); - module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Escalated) + ); - // Store request data - uint256 _minVotesForQuorum = 1; - uint256 _votingTimeWindow = 40_000; + module.forTest_setStartTime(_disputeId, 100_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); + module.castVote(mockRequest, mockDispute, _numberOfVotes); } } @@ -285,21 +259,27 @@ 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; + function test_resolveDispute(uint16 _minVotesForQuorum) public { + mockRequest.resolutionModuleData = abi.encode( + IERC20ResolutionModule.RequestParameters({ + votingToken: token, + minVotesForQuorum: _minVotesForQuorum, + timeUntilDeadline: votingTimeWindow + }) + ); - // Store request data - uint256 _votingTimeWindow = 40_000; + bytes32 _requestId = _getId(mockRequest); + mockDispute.requestId = _requestId; + bytes32 _disputeId = _getId(mockDispute); // Store escalation data with `startTime` 100_000 and votes 0 - module.forTest_setEscalation(_disputeId, IERC20ResolutionModule.Escalation({startTime: 100_000, totalVotes: 0})); + module.forTest_setStartTime(_disputeId, 100_000); uint256 _votersAmount = 5; // Make 5 addresses cast 100 votes each - uint256 _totalVotesCast = _populateVoters(_requestId, _disputeId, _votersAmount, 100, _request); + uint256 _totalVotesCast = 100 * _votersAmount; + _populateVoters(_disputeId, _votersAmount, 100); // Warp to resolving phase vm.warp(150_000); @@ -312,6 +292,10 @@ contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest { } } + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Escalated) + ); + // If quorum reached, check for dispute status update and event emission IOracle.DisputeStatus _newStatus = _totalVotesCast >= _minVotesForQuorum ? IOracle.DisputeStatus.Won : IOracle.DisputeStatus.Lost; @@ -319,7 +303,7 @@ contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest { // Mock and expect IOracle.updateDisputeStatus to be called _mockAndExpect( address(oracle), - abi.encodeCall(IOracle.updateDisputeStatus, (_request, mockResponse, mockDispute, _newStatus)), + abi.encodeCall(IOracle.updateDisputeStatus, (mockRequest, mockResponse, mockDispute, _newStatus)), abi.encode() ); @@ -329,37 +313,36 @@ contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest { // Check: does revert if called by address != oracle? vm.expectRevert(IModule.Module_OnlyOracle.selector); - module.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + module.resolveDispute(_disputeId, mockRequest, mockResponse, mockDispute); vm.prank(address(oracle)); - module.resolveDispute(_disputeId, _request, mockResponse, mockDispute); + module.resolveDispute(_disputeId, mockRequest, mockResponse, mockDispute); } /** * @notice Test that `resolveDispute` reverts if called during voting phase. */ - function test_revertIfOnGoingVotePhase( - IOracle.Request calldata _request, - bytes32 _disputeId, - uint256 _timestamp - ) public { + function test_revertIfOnGoingVotePhase(uint256 _timestamp) public { _timestamp = bound(_timestamp, 500_000, 999_999); - // Store mock dispute and mock calls - bytes32 _requestId = _getId(_request); - mockDispute.requestId = _requestId; + uint256 _minVotesForQuorum = 1; + uint256 _votingTimeWindow = 500_000; - module.forTest_setEscalation( - _disputeId, - IERC20ResolutionModule.Escalation({ - startTime: 500_000, - totalVotes: 0 // Initial amount of votes + mockRequest.resolutionModuleData = abi.encode( + IERC20ResolutionModule.RequestParameters({ + votingToken: token, + minVotesForQuorum: _minVotesForQuorum, + timeUntilDeadline: _votingTimeWindow }) ); + mockDispute.requestId = _getId(mockRequest); + bytes32 _disputeId = _getId(mockDispute); - // Store request data - uint256 _minVotesForQuorum = 1; - uint256 _votingTimeWindow = 500_000; + module.forTest_setStartTime(_disputeId, 500_000); + + _mockAndExpect( + address(oracle), abi.encodeCall(IOracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Escalated) + ); // Jump to timestamp vm.warp(_timestamp); @@ -367,7 +350,7 @@ contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest { // 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); + module.resolveDispute(_disputeId, mockRequest, mockResponse, mockDispute); } } @@ -375,21 +358,16 @@ 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); + function test_getVoters(bytes32 _disputeId) public { + bytes32 _requestId = _getId(mockRequest); 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})); + module.forTest_setStartTime(_disputeId, 100_000); uint256 _votersAmount = 3; // Make 3 addresses cast 100 votes each - _populateVoters(_requestId, _disputeId, _votersAmount, 100, _request); + _populateVoters(_disputeId, _votersAmount, 100); address[] memory _votersArray = module.getVoters(_disputeId);