diff --git a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol index b2bfad3f..e2f02639 100644 --- a/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol +++ b/solidity/contracts/modules/resolution/ERC20ResolutionModule.sol @@ -71,7 +71,8 @@ contract ERC20ResolutionModule is Module, IERC20ResolutionModule { _voters[_disputeId].add(msg.sender); escalations[_disputeId].totalVotes += _numberOfVotes; - _params.accountingExtension.bond(msg.sender, _dispute.requestId, _params.votingToken, _numberOfVotes); + _params.votingToken.safeTransferFrom(msg.sender, address(this), _numberOfVotes); + emit VoteCast(msg.sender, _disputeId, _numberOfVotes); } @@ -120,7 +121,7 @@ contract ERC20ResolutionModule is Module, IERC20ResolutionModule { // Transfer the tokens back to the voter uint256 _amount = votes[_disputeId][msg.sender]; - _params.accountingExtension.release(msg.sender, _dispute.requestId, _params.votingToken, _amount); + _params.votingToken.safeTransfer(msg.sender, _amount); emit VoteClaimed(msg.sender, _disputeId, _amount); } diff --git a/solidity/contracts/modules/resolution/PrivateERC20ResolutionModule.sol b/solidity/contracts/modules/resolution/PrivateERC20ResolutionModule.sol index f56dd714..9e598e41 100644 --- a/solidity/contracts/modules/resolution/PrivateERC20ResolutionModule.sol +++ b/solidity/contracts/modules/resolution/PrivateERC20ResolutionModule.sol @@ -138,6 +138,8 @@ contract PrivateERC20ResolutionModule is Module, IPrivateERC20ResolutionModule { } address _voter; + // review: should we allow an alternative to this gas consuming approach? + // we could add a configurable param to skip the forced token mass-transfer, and let users do it manually. uint256 _votersLength = _voters[_disputeId].length(); for (uint256 _i; _i < _votersLength;) { _voter = _voters[_disputeId].at(_i); diff --git a/solidity/test/integration/IntegrationBase.sol b/solidity/test/integration/IntegrationBase.sol index d9fe9d64..6d4325a9 100644 --- a/solidity/test/integration/IntegrationBase.sol +++ b/solidity/test/integration/IntegrationBase.sol @@ -6,6 +6,7 @@ pragma solidity ^0.8.19; import {console} from 'forge-std/console.sol'; import {IOracle, Oracle} from '@defi-wonderland/prophet-core/solidity/contracts/Oracle.sol'; +import {IValidator} from '@defi-wonderland/prophet-core/solidity/interfaces/IValidator.sol'; import {IDisputeModule} from '@defi-wonderland/prophet-core/solidity/interfaces/modules/dispute/IDisputeModule.sol'; import {IFinalityModule} from '@defi-wonderland/prophet-core/solidity/interfaces/modules/finality/IFinalityModule.sol'; import {IRequestModule} from '@defi-wonderland/prophet-core/solidity/interfaces/modules/request/IRequestModule.sol'; diff --git a/solidity/test/integration/PrivaterResolution.t.sol b/solidity/test/integration/PrivaterResolution.t.sol new file mode 100644 index 00000000..a0bd6fe0 --- /dev/null +++ b/solidity/test/integration/PrivaterResolution.t.sol @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { + IPrivateERC20ResolutionModule, + PrivateERC20ResolutionModule +} from '../../contracts/modules/resolution/PrivateERC20ResolutionModule.sol'; +import './IntegrationBase.sol'; + +contract Integration_PrivateResolution is IntegrationBase { + PrivateERC20ResolutionModule public privateERC20ResolutionModule; + + IERC20 internal _votingToken; + uint256 internal _minimumQuorum = 1000; + uint256 internal _committingTimeWindow = 1 days; + uint256 internal _revealingTimeWindow = 1 days; + + address internal _voterA = makeAddr('voter-a'); + address internal _voterB = makeAddr('voter-b'); + bytes32 internal _disputeId; + + bytes32 internal _goodSalt = keccak256('salty'); + + function setUp() public override { + super.setUp(); + + vm.prank(governance); + privateERC20ResolutionModule = new PrivateERC20ResolutionModule(oracle); + + _votingToken = IERC20(address(weth)); + + mockRequest.resolutionModule = address(privateERC20ResolutionModule); + mockRequest.resolutionModuleData = abi.encode( + IPrivateERC20ResolutionModule.RequestParameters({ + accountingExtension: _accountingExtension, + votingToken: _votingToken, + minVotesForQuorum: _minimumQuorum, + committingTimeWindow: _committingTimeWindow, + revealingTimeWindow: _revealingTimeWindow + }) + ); + + _deposit(_accountingExtension, requester, usdc, _expectedReward); + _deposit(_accountingExtension, proposer, usdc, _expectedBondSize); + _deposit(_accountingExtension, disputer, usdc, _expectedBondSize); + + _setupDispute(); + } + + function test_resolve_noVotes() public { + // expect call to startResolution + vm.expectCall( + address(privateERC20ResolutionModule), + abi.encodeCall( + IPrivateERC20ResolutionModule.startResolution, (_disputeId, mockRequest, mockResponse, mockDispute) + ) + ); + + oracle.escalateDispute(mockRequest, mockResponse, mockDispute); + + // expect call to update dispute' status as lost + vm.expectCall( + address(oracle), + abi.encodeCall(IOracle.updateDisputeStatus, (mockRequest, mockResponse, mockDispute, IOracle.DisputeStatus.Lost)) + ); + + // expect call to resolveDispute + vm.expectCall( + address(privateERC20ResolutionModule), + abi.encodeCall(IPrivateERC20ResolutionModule.resolveDispute, (_disputeId, mockRequest, mockResponse, mockDispute)) + ); + + (uint256 _startTime, uint256 _totalVotes) = privateERC20ResolutionModule.escalations(_disputeId); + assertEq(_startTime, block.timestamp); + assertEq(_totalVotes, 0); + + // expect revert when try to resolve before the committing phase is over + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_OnGoingCommittingPhase.selector); + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + + // warp past the committing phase + vm.warp(block.timestamp + _committingTimeWindow + 1); + // expect revert when try to resolve before the revealing phase is over + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_OnGoingRevealingPhase.selector); + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + + // warp past the revealing phase + vm.warp(block.timestamp + _revealingTimeWindow); + + // successfully resolve the dispute + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + + (_startTime, _totalVotes) = privateERC20ResolutionModule.escalations(_disputeId); + assertEq(_totalVotes, 0); + } + + function test_resolve_enoughVotes() public { + oracle.escalateDispute(mockRequest, mockResponse, mockDispute); + + // we have enough votes to reach the quorum + uint256 _votes = _minimumQuorum + 1; + + // expect call to update dispute' status as won + vm.expectCall( + address(oracle), + abi.encodeCall(IOracle.updateDisputeStatus, (mockRequest, mockResponse, mockDispute, IOracle.DisputeStatus.Won)) + ); + + // expect call to transfer tokens from voter to module + vm.expectCall( + address(_votingToken), + abi.encodeCall(IERC20.transferFrom, (_voterA, address(privateERC20ResolutionModule), _votes)) + ); + + // expect call to transfer tokens to voter + vm.expectCall(address(_votingToken), abi.encodeCall(IERC20.transfer, (_voterA, _votes))); + + // warp into the commiting window + vm.warp(block.timestamp + 1); + + deal(address(_votingToken), _voterA, _votes); + vm.startPrank(_voterA); + _votingToken.approve(address(privateERC20ResolutionModule), _votes); + bytes32 _commitment = privateERC20ResolutionModule.computeCommitment(_disputeId, _votes, _goodSalt); + privateERC20ResolutionModule.commitVote(mockRequest, mockDispute, _commitment); + + // warp past the commiting window + vm.warp(block.timestamp + _committingTimeWindow + 1); + + // assert has enough voting tokens + assertEq(_votingToken.balanceOf(_voterA), _votes); + privateERC20ResolutionModule.revealVote(mockRequest, mockDispute, _votes, _goodSalt); + // assert voting tokens were transfered + assertEq(_votingToken.balanceOf(_voterA), 0); + assertEq(_votingToken.balanceOf(address(privateERC20ResolutionModule)), _votes); + + vm.stopPrank(); + + // warp past the revealing window + vm.warp(block.timestamp + _revealingTimeWindow); + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + + // assert voting tokens were transfered back to the voter. + assertEq(_votingToken.balanceOf(_voterA), _votes); + assertEq(_votingToken.balanceOf(address(privateERC20ResolutionModule)), 0); + } + + function test_resolve_notEnoughVotes() public { + oracle.escalateDispute(mockRequest, mockResponse, mockDispute); + + uint256 _votes = _minimumQuorum - 1; + + vm.expectCall( + address(oracle), + abi.encodeCall(IOracle.updateDisputeStatus, (mockRequest, mockResponse, mockDispute, IOracle.DisputeStatus.Lost)) + ); + + vm.expectCall( + address(_votingToken), + abi.encodeCall(IERC20.transferFrom, (_voterA, address(privateERC20ResolutionModule), _votes)) + ); + + vm.expectCall(address(_votingToken), abi.encodeCall(IERC20.transfer, (_voterA, _votes))); + + // warp into the commiting window + vm.warp(block.timestamp + 1); + + deal(address(_votingToken), _voterA, _votes); + + vm.startPrank(_voterA); + + _votingToken.approve(address(privateERC20ResolutionModule), _votes); + bytes32 _commitment = privateERC20ResolutionModule.computeCommitment(_disputeId, _votes, _goodSalt); + privateERC20ResolutionModule.commitVote(mockRequest, mockDispute, _commitment); + + // warp into the committing phase + vm.warp(block.timestamp + _committingTimeWindow); + privateERC20ResolutionModule.revealVote(mockRequest, mockDispute, _votes, _goodSalt); + + vm.stopPrank(); + + vm.warp(block.timestamp + _revealingTimeWindow); + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + } + + function test_resolve_noEscalation() public { + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_AlreadyResolved.selector); + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + } + + function test_zeroVotes() public { + oracle.escalateDispute(mockRequest, mockResponse, mockDispute); + + uint256 _votes = 0; + + // expect call to transfer `0` tokens to voterB + vm.expectCall(address(_votingToken), abi.encodeCall(IERC20.transfer, (_voterA, 0))); + + vm.startPrank(_voterA); + bytes32 _commitment = privateERC20ResolutionModule.computeCommitment(_disputeId, _votes, _goodSalt); + privateERC20ResolutionModule.commitVote(mockRequest, mockDispute, _commitment); + vm.stopPrank(); + + // warp past the commiting phase + vm.warp(block.timestamp + _committingTimeWindow + 1); + + // expert to revert because the sender is not correct + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_WrongRevealData.selector); + vm.prank(_voterB); + privateERC20ResolutionModule.revealVote(mockRequest, mockDispute, _votes, _goodSalt); + + vm.prank(_voterA); + privateERC20ResolutionModule.revealVote(mockRequest, mockDispute, _votes, _goodSalt); + + vm.warp(block.timestamp + _revealingTimeWindow + 1); + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + + assertEq(_votingToken.balanceOf(_voterA), 0); + assertEq(_votingToken.balanceOf(address(privateERC20ResolutionModule)), 0); + } + + function test_commit() public { + uint256 _votes = 0; + + vm.startPrank(_voterA); + bytes32 _commitment = privateERC20ResolutionModule.computeCommitment(_disputeId, _votes, _goodSalt); + + // expect revert when trying to commit a vote into an already resolved dispute + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_AlreadyResolved.selector); + privateERC20ResolutionModule.commitVote(mockRequest, mockDispute, _commitment); + + oracle.escalateDispute(mockRequest, mockResponse, mockDispute); + + // expect revert when trying to commit a vote with an empty commitment + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_EmptyCommitment.selector); + privateERC20ResolutionModule.commitVote(mockRequest, mockDispute, bytes32('')); + + // successfully commit a vote + privateERC20ResolutionModule.commitVote(mockRequest, mockDispute, _commitment); + + // warp past the committing phase + vm.warp(block.timestamp + _committingTimeWindow); + + // expect revert when trying to commit after the committing phase is over + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_CommittingPhaseOver.selector); + privateERC20ResolutionModule.commitVote(mockRequest, mockDispute, _commitment); + + vm.stopPrank(); + } + + function test_reveal() public { + uint256 _votes = 0; + + vm.startPrank(_voterA); + + // expect revert when reveal a commit into a not escalated dispute + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_DisputeNotEscalated.selector); + privateERC20ResolutionModule.revealVote(mockRequest, mockDispute, _votes, _goodSalt); + + bytes32 _commitment = privateERC20ResolutionModule.computeCommitment(_disputeId, _votes, _goodSalt); + + // escalate and commit a vote using `_goodSalt` + oracle.escalateDispute(mockRequest, mockResponse, mockDispute); + privateERC20ResolutionModule.commitVote(mockRequest, mockDispute, _commitment); + + // expect revert when trying to reveal vote during committing phase + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_OnGoingCommittingPhase.selector); + privateERC20ResolutionModule.revealVote(mockRequest, mockDispute, _votes, _goodSalt); + + // warp past the committing phase + vm.warp(block.timestamp + _committingTimeWindow + 1); + + // expect revert when trying to reveal a vote using the incorrect salt + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_WrongRevealData.selector); + privateERC20ResolutionModule.revealVote(mockRequest, mockDispute, _votes, bytes32('bad-salt')); + + // succesfully reveal a vote + privateERC20ResolutionModule.revealVote(mockRequest, mockDispute, _votes, _goodSalt); + + // warp past the revealing phase + vm.warp(block.timestamp + _revealingTimeWindow); + + // expect revert when trying to reveal a vote phase the revealing phase + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_RevealingPhaseOver.selector); + privateERC20ResolutionModule.revealVote(mockRequest, mockDispute, _votes, _goodSalt); + + vm.stopPrank(); + } + + function test_multipleVoters() public { + oracle.escalateDispute(mockRequest, mockResponse, mockDispute); + + uint256 _votes = 10; + + // expect call to transfer `0` tokens to voterB + vm.expectCall(address(_votingToken), abi.encodeCall(IERC20.transfer, (_voterA, _votes)), 1); + vm.expectCall(address(_votingToken), abi.encodeCall(IERC20.transfer, (_voterB, _votes)), 1); + + // voterA cast votes + deal(address(_votingToken), _voterA, _votes); + vm.startPrank(_voterA); + _votingToken.approve(address(privateERC20ResolutionModule), _votes); + privateERC20ResolutionModule.commitVote( + mockRequest, mockDispute, privateERC20ResolutionModule.computeCommitment(_disputeId, _votes, _goodSalt) + ); + vm.stopPrank(); + + // voterB cast votes + deal(address(_votingToken), _voterB, _votes); + vm.startPrank(_voterB); + _votingToken.approve(address(privateERC20ResolutionModule), _votes); + privateERC20ResolutionModule.commitVote( + mockRequest, mockDispute, privateERC20ResolutionModule.computeCommitment(_disputeId, _votes, _goodSalt) + ); + vm.stopPrank(); + + // warp past the voting phase + vm.warp(block.timestamp + _committingTimeWindow + 1); + + vm.prank(_voterA); + privateERC20ResolutionModule.revealVote(mockRequest, mockDispute, _votes, _goodSalt); + + // expect revert because the salt is not correct + vm.expectRevert(IPrivateERC20ResolutionModule.PrivateERC20ResolutionModule_WrongRevealData.selector); + vm.prank(_voterB); + privateERC20ResolutionModule.revealVote(mockRequest, mockDispute, _votes, bytes32('bad salt')); + + vm.prank(_voterB); + privateERC20ResolutionModule.revealVote(mockRequest, mockDispute, _votes, _goodSalt); + + vm.stopPrank(); + + vm.warp(block.timestamp + _revealingTimeWindow + 1); + + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + + // both voters get their votes back + assertEq(_votingToken.balanceOf(_voterA), _votes); + assertEq(_votingToken.balanceOf(_voterB), _votes); + } + + function _setupDispute() internal { + _resetMockIds(); + + _createRequest(); + _proposeResponse(); + _disputeId = _disputeResponse(); + } +} diff --git a/solidity/test/integration/Resolution.t.sol b/solidity/test/integration/Resolution.t.sol new file mode 100644 index 00000000..ef03a208 --- /dev/null +++ b/solidity/test/integration/Resolution.t.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import { + ERC20ResolutionModule, IERC20ResolutionModule +} from '../../contracts/modules/resolution/ERC20ResolutionModule.sol'; +import './IntegrationBase.sol'; + +contract Integration_Resolution is IntegrationBase { + ERC20ResolutionModule internal _erc20ResolutionModule; + + IERC20 internal _votingToken; + uint256 internal _minimumQuorum = 1000; + uint256 internal _timeUntilDeadline = 1 days; + + address internal _voterA = makeAddr('voter-a'); + address internal _voterB = makeAddr('voter-b'); + bytes32 internal _disputeId; + + function setUp() public override { + super.setUp(); + + vm.prank(governance); + _erc20ResolutionModule = new ERC20ResolutionModule(oracle); + + _votingToken = IERC20(address(weth)); + + mockRequest.resolutionModule = address(_erc20ResolutionModule); + mockRequest.resolutionModuleData = abi.encode( + IERC20ResolutionModule.RequestParameters({ + accountingExtension: _accountingExtension, + votingToken: _votingToken, + minVotesForQuorum: _minimumQuorum, + timeUntilDeadline: _timeUntilDeadline + }) + ); + + _deposit(_accountingExtension, requester, usdc, _expectedReward); + _deposit(_accountingExtension, proposer, usdc, _expectedBondSize); + _deposit(_accountingExtension, disputer, usdc, _expectedBondSize); + + _setupDispute(); + } + + function test_resolve_noVotes() public { + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_AlreadyResolved.selector); + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + + oracle.escalateDispute(mockRequest, mockResponse, mockDispute); + + // expect call to update dispute' status as lost + vm.expectCall( + address(oracle), + abi.encodeCall(IOracle.updateDisputeStatus, (mockRequest, mockResponse, mockDispute, IOracle.DisputeStatus.Lost)) + ); + + (uint256 _startTime, uint256 _totalVotes) = _erc20ResolutionModule.escalations(_disputeId); + assertEq(_startTime, block.timestamp); + assertEq(_totalVotes, 0); + + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_OnGoingVotingPhase.selector); + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + + vm.warp(block.timestamp + _timeUntilDeadline); + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + + (_startTime,) = _erc20ResolutionModule.escalations(_disputeId); + assertEq(_totalVotes, 0); + } + + function test_resolve_notEnoughVotes() public { + oracle.escalateDispute(mockRequest, mockResponse, mockDispute); + + uint256 _votes = _minimumQuorum - 1; + + vm.expectCall( + address(oracle), + abi.encodeCall(IOracle.updateDisputeStatus, (mockRequest, mockResponse, mockDispute, IOracle.DisputeStatus.Lost)) + ); + + vm.expectCall( + address(_votingToken), abi.encodeCall(IERC20.transferFrom, (_voterA, address(_erc20ResolutionModule), _votes)) + ); + + vm.expectCall(address(_votingToken), abi.encodeCall(IERC20.transfer, (_voterA, _votes))); + + deal(address(_votingToken), _voterA, _votes); + vm.startPrank(_voterA); + _votingToken.approve(address(_erc20ResolutionModule), _votes); + _erc20ResolutionModule.castVote(mockRequest, mockDispute, _votes); + vm.stopPrank(); + + vm.warp(block.timestamp + _timeUntilDeadline); + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + + vm.prank(_voterA); + _erc20ResolutionModule.claimVote(mockRequest, mockDispute); + } + + function test_resolve_enoughVotes() public { + uint256 _votes = _minimumQuorum + 1; + + oracle.escalateDispute(mockRequest, mockResponse, mockDispute); + + // expect call to update dispute' status as won + vm.expectCall( + address(oracle), + abi.encodeCall(IOracle.updateDisputeStatus, (mockRequest, mockResponse, mockDispute, IOracle.DisputeStatus.Won)) + ); + + // expect call to transfer tokens from voter to module + vm.expectCall( + address(_votingToken), abi.encodeCall(IERC20.transferFrom, (_voterA, address(_erc20ResolutionModule), _votes)) + ); + // expect call to transfer `_votes` tokens to voterA + vm.expectCall(address(_votingToken), abi.encodeCall(IERC20.transfer, (_voterA, _votes))); + + deal(address(_votingToken), _voterA, _votes); + vm.startPrank(_voterA); + _votingToken.approve(address(_erc20ResolutionModule), _votes); + _erc20ResolutionModule.castVote(mockRequest, mockDispute, _votes); + vm.stopPrank(); + + // revert when voter claims before the correct timestamp + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_OnGoingVotingPhase.selector); + vm.prank(_voterA); + _erc20ResolutionModule.claimVote(mockRequest, mockDispute); + + // warp past the voting phase + vm.warp(block.timestamp + _timeUntilDeadline); + + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_VotingPhaseOver.selector); + vm.prank(_voterA); + _erc20ResolutionModule.castVote(mockRequest, mockDispute, _votes); + + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + + // voterA claims back casted votes. + vm.prank(_voterA); + _erc20ResolutionModule.claimVote(mockRequest, mockDispute); + } + + function test_resolve_noEscalation() public { + // no escalation + + // fixme: expected ERC20ResolutionModule_DisputeNotEscalated + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_AlreadyResolved.selector); + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + } + + function test_invalidRequest() public { + oracle.escalateDispute(mockRequest, mockResponse, mockDispute); + + mockRequest.nonce += 1; + _resetMockIds(); + + vm.expectRevert(IValidator.Validator_InvalidDispute.selector); + + vm.prank(_voterA); + _erc20ResolutionModule.castVote(mockRequest, mockDispute, 0); + } + + function test_zeroVotes() public { + oracle.escalateDispute(mockRequest, mockResponse, mockDispute); + + // expect call to transfer `0` tokens to voterB + vm.expectCall(address(_votingToken), abi.encodeCall(IERC20.transfer, (_voterB, 0))); + + // deal(address(_votingToken), _voterA, _votes); + vm.startPrank(_voterA); + // _votingToken.approve(address(_erc20ResolutionModule), _votes); + _erc20ResolutionModule.castVote(mockRequest, mockDispute, 0); + vm.stopPrank(); + + // warp past the voting phase + vm.warp(block.timestamp + _timeUntilDeadline); + + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_VotingPhaseOver.selector); + vm.prank(_voterA); + _erc20ResolutionModule.castVote(mockRequest, mockDispute, 0); + + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + // non-participant user claim votes, it will receive 0 tokens + // todo: should we revert on this case? + vm.prank(_voterB); + _erc20ResolutionModule.claimVote(mockRequest, mockDispute); + + // voterA claims back casted votes. + vm.prank(_voterA); + _erc20ResolutionModule.claimVote(mockRequest, mockDispute); + + // any-user cast 0 votes into an already resolved disputed + vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_AlreadyResolved.selector); + _erc20ResolutionModule.castVote(mockRequest, mockDispute, 0); + } + + function test_multipleVoters() public { + oracle.escalateDispute(mockRequest, mockResponse, mockDispute); + + uint256 _votes = 10; + + // expect call to transfer `0` tokens to voterB + vm.expectCall(address(_votingToken), abi.encodeCall(IERC20.transfer, (_voterA, _votes)), 2); + + // voterA cast votes + deal(address(_votingToken), _voterA, _votes); + vm.startPrank(_voterA); + _votingToken.approve(address(_erc20ResolutionModule), _votes); + _erc20ResolutionModule.castVote(mockRequest, mockDispute, _votes); + vm.stopPrank(); + + // voterB cast votes + deal(address(_votingToken), _voterB, _votes); + vm.startPrank(_voterB); + _votingToken.approve(address(_erc20ResolutionModule), _votes); + _erc20ResolutionModule.castVote(mockRequest, mockDispute, _votes); + vm.stopPrank(); + + // warp past the voting phase + vm.warp(block.timestamp + _timeUntilDeadline); + + oracle.resolveDispute(mockRequest, mockResponse, mockDispute); + + // voterA claims back casted votes. + vm.startPrank(_voterA); + _erc20ResolutionModule.claimVote(mockRequest, mockDispute); + _erc20ResolutionModule.claimVote(mockRequest, mockDispute); + vm.stopPrank(); + + // voterA claims its votes twice + assertEq(_votingToken.balanceOf(_voterA), _votes * 2); + + // now voterB claim fails due to lack of balance. + vm.expectRevert(); + vm.prank(_voterB); + _erc20ResolutionModule.claimVote(mockRequest, mockDispute); + } + + function _setupDispute() internal { + _resetMockIds(); + + _createRequest(); + _proposeResponse(); + _disputeId = _disputeResponse(); + } +} diff --git a/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol b/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol index 1ea5bba7..4687a3e0 100644 --- a/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol +++ b/solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol @@ -178,13 +178,9 @@ contract ERC20ResolutionModule_Unit_CastVote is BaseTest { // Store mock escalation data with startTime 100_000 module.forTest_setStartTime(_disputeId, 100_000); - // Mock and expect the bond to be placed + // Mock and expect the token transferFrom _mockAndExpect( - address(accountingExtension), - abi.encodeWithSignature( - 'bond(address,bytes32,address,uint256)', _voter, _dispute.requestId, token, _amountOfVotes - ), - abi.encode() + address(token), abi.encodeCall(IERC20.transferFrom, (_voter, address(module), _amountOfVotes)), abi.encode(true) ); _mockAndExpect( @@ -471,12 +467,8 @@ contract ERC20ResolutionModule_Unit_ClaimVote is BaseTest { // Mock and expect IOracle.disputeCreatedAt to be called _mockAndExpect(address(oracle), abi.encodeCall(IOracle.disputeCreatedAt, (_disputeId)), abi.encode(1)); - // Expect the bond to be released - _mockAndExpect( - address(accountingExtension), - abi.encodeCall(accountingExtension.release, (_voter, _dispute.requestId, token, _amount)), - abi.encode() - ); + // Mock and expect voting token transfer + _mockAndExpect(address(token), abi.encodeCall(IERC20.transfer, (_voter, _amount)), abi.encode(true)); vm.warp(block.timestamp + 1000);