Skip to content

Commit

Permalink
refactor: pull funds in ERC20ResolutionModule (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
gas1cent authored Nov 27, 2023
1 parent ef77ae3 commit db865ab
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 36 deletions.
36 changes: 20 additions & 16 deletions solidity/contracts/modules/resolution/ERC20ResolutionModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ contract ERC20ResolutionModule is Module, IERC20ResolutionModule {
_voters[_disputeId].add(msg.sender);
escalations[_disputeId].totalVotes += _numberOfVotes;

_params.votingToken.safeTransferFrom(msg.sender, address(this), _numberOfVotes);
_params.accountingExtension.bond(msg.sender, _dispute.requestId, _params.votingToken, _numberOfVotes);
emit VoteCast(msg.sender, _disputeId, _numberOfVotes);
}

Expand All @@ -83,43 +83,47 @@ contract ERC20ResolutionModule is Module, IERC20ResolutionModule {
IOracle.Response calldata _response,
IOracle.Dispute calldata _dispute
) external onlyOracle {
// 0. Check disputeId actually exists and that it isn't resolved already
// Check disputeId actually exists and that it isn't resolved already
if (ORACLE.disputeStatus(_disputeId) != IOracle.DisputeStatus.Escalated) {
revert ERC20ResolutionModule_AlreadyResolved();
}

// 1. Check that the dispute is actually escalated
// 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
// 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
// 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;
/// @inheritdoc IERC20ResolutionModule
function claimVote(IOracle.Request calldata _request, IOracle.Dispute calldata _dispute) external {
bytes32 _disputeId = _getId(_dispute);
Escalation memory _escalation = escalations[_disputeId];

// 6. Return tokens
for (uint256 _i; _i < _votersLength;) {
address _voter = __voters[_i];
_params.votingToken.safeTransfer(_voter, votes[_disputeId][_voter]);
unchecked {
++_i;
}
}
// 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();

// Transfer the tokens back to the voter
uint256 _amount = votes[_disputeId][msg.sender];
_params.accountingExtension.release(msg.sender, _dispute.requestId, _params.votingToken, _amount);

emit VoteClaimed(msg.sender, _disputeId, _amount);
}

/// @inheritdoc IERC20ResolutionModule
Expand Down
16 changes: 16 additions & 0 deletions solidity/interfaces/modules/resolution/IERC20ResolutionModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {IResolutionModule} from
'@defi-wonderland/prophet-core-contracts/solidity/interfaces/modules/resolution/IResolutionModule.sol';
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';

import {IAccountingExtension} from '../../extensions/IAccountingExtension.sol';

/**
* @title ERC20ResolutionModule
* @notice This contract allows for disputes to be resolved by a voting process.
Expand Down Expand Up @@ -34,6 +36,11 @@ interface IERC20ResolutionModule is IResolutionModule {
*/
event VotingPhaseStarted(uint256 _startTime, bytes32 _disputeId);

/**
* @notice Emitted when the voter gets back their bond
*/
event VoteClaimed(address _voter, bytes32 _disputeId, uint256 _amount);

/*///////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -79,6 +86,7 @@ interface IERC20ResolutionModule is IResolutionModule {
* @param timeUntilDeadline The time until the voting phase ends
*/
struct RequestParameters {
IAccountingExtension accountingExtension;
IERC20 votingToken;
uint256 minVotesForQuorum;
uint256 timeUntilDeadline;
Expand Down Expand Up @@ -165,6 +173,14 @@ interface IERC20ResolutionModule is IResolutionModule {
IOracle.Dispute calldata _dispute
) external;

/**
* @notice Releases the voter's bond
*
* @param _request The request for which the dispute was created
* @param _dispute The resolved dispute
*/
function claimVote(IOracle.Request calldata _request, IOracle.Dispute calldata _dispute) external;

/**
* @notice Gets the voters of a dispute
*
Expand Down
107 changes: 87 additions & 20 deletions solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
IERC20ResolutionModule
} from '../../../../contracts/modules/resolution/ERC20ResolutionModule.sol';

import {IAccountingExtension} from '../../../../interfaces/extensions/IAccountingExtension.sol';

contract ForTest_ERC20ResolutionModule is ERC20ResolutionModule {
using EnumerableSet for EnumerableSet.AddressSet;

Expand Down Expand Up @@ -43,27 +45,24 @@ contract BaseTest is Test, Helpers {
// The target contract
ForTest_ERC20ResolutionModule public module;
// A mock oracle
IOracle public oracle;
IOracle public oracle = IOracle(_mockContract('Oracle'));
// A mock token
IERC20 public token;
IERC20 public token = IERC20(_mockContract('Token'));
// Mock accounting extension
IAccountingExtension public accountingExtension = IAccountingExtension(_mockContract('AccountingExtension'));

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);
event VoteClaimed(address _voter, bytes32 _disputeId, uint256 _amount);

/**
* @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');

function setUp() public virtual {
module = new ForTest_ERC20ResolutionModule(oracle);
}

Expand Down Expand Up @@ -95,12 +94,13 @@ contract ERC20ResolutionModule_Unit_ModuleData is BaseTest {
uint256 _votingTimeWindow
) public {
// Mock data
bytes memory _requestData = abi.encode(_token, _minVotesForQuorum, _votingTimeWindow);
bytes memory _requestData = abi.encode(address(accountingExtension), _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.accountingExtension), address(accountingExtension));
assertEq(address(_params.votingToken), _token);
assertEq(_params.minVotesForQuorum, _minVotesForQuorum);
assertEq(_params.timeUntilDeadline, _votingTimeWindow);
Expand Down Expand Up @@ -145,6 +145,7 @@ contract ERC20ResolutionModule_Unit_CastVote is BaseTest {

mockRequest.resolutionModuleData = abi.encode(
IERC20ResolutionModule.RequestParameters({
accountingExtension: accountingExtension,
votingToken: token,
minVotesForQuorum: _minVotesForQuorum,
timeUntilDeadline: votingTimeWindow
Expand All @@ -156,9 +157,13 @@ contract ERC20ResolutionModule_Unit_CastVote is BaseTest {
// Store mock escalation data with startTime 100_000
module.forTest_setStartTime(_disputeId, 100_000);

// Mock and expect IERC20.transferFrom to be called
// Mock and expect the bond to be placed
_mockAndExpect(
address(token), abi.encodeCall(IERC20.transferFrom, (_voter, address(module), _amountOfVotes)), abi.encode()
address(accountingExtension),
abi.encodeWithSignature(
'bond(address,bytes32,address,uint256)', _voter, mockDispute.requestId, token, _amountOfVotes
),
abi.encode()
);

_mockAndExpect(
Expand Down Expand Up @@ -201,6 +206,7 @@ contract ERC20ResolutionModule_Unit_CastVote is BaseTest {
function test_revertIfAlreadyResolved(uint256 _amountOfVotes, uint256 _votingTimeWindow) public {
mockRequest.resolutionModuleData = abi.encode(
IERC20ResolutionModule.RequestParameters({
accountingExtension: accountingExtension,
votingToken: token,
minVotesForQuorum: _amountOfVotes,
timeUntilDeadline: _votingTimeWindow
Expand Down Expand Up @@ -230,6 +236,7 @@ contract ERC20ResolutionModule_Unit_CastVote is BaseTest {

mockRequest.resolutionModuleData = abi.encode(
IERC20ResolutionModule.RequestParameters({
accountingExtension: accountingExtension,
votingToken: token,
minVotesForQuorum: _minVotesForQuorum,
timeUntilDeadline: votingTimeWindow
Expand Down Expand Up @@ -262,6 +269,7 @@ contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest {
function test_resolveDispute(uint16 _minVotesForQuorum) public {
mockRequest.resolutionModuleData = abi.encode(
IERC20ResolutionModule.RequestParameters({
accountingExtension: accountingExtension,
votingToken: token,
minVotesForQuorum: _minVotesForQuorum,
timeUntilDeadline: votingTimeWindow
Expand All @@ -284,14 +292,6 @@ contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest {
// 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;
}
}

_mockAndExpect(
address(oracle), abi.encodeCall(IOracle.disputeStatus, (_disputeId)), abi.encode(IOracle.DisputeStatus.Escalated)
);
Expand Down Expand Up @@ -330,6 +330,7 @@ contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest {

mockRequest.resolutionModuleData = abi.encode(
IERC20ResolutionModule.RequestParameters({
accountingExtension: accountingExtension,
votingToken: token,
minVotesForQuorum: _minVotesForQuorum,
timeUntilDeadline: _votingTimeWindow
Expand All @@ -354,6 +355,72 @@ contract ERC20ResolutionModule_Unit_ResolveDispute is BaseTest {
}
}

contract ERC20ResolutionModule_Unit_ClaimVote is BaseTest {
/**
* @notice Reverts if the vote is still ongoing
*/
function test_revertIfVoteIsOnGoing(address _voter, uint256 _amount) public {

Check warning on line 362 in solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol

View workflow job for this annotation

GitHub Actions / Run Linters (16.x)

Variable "_amount" is unused
mockRequest.resolutionModuleData = abi.encode(
IERC20ResolutionModule.RequestParameters({
accountingExtension: accountingExtension,
votingToken: token,
minVotesForQuorum: 1,
timeUntilDeadline: 1000
})
);

mockDispute.requestId = _getId(mockRequest);
bytes32 _disputeId = _getId(mockDispute);

Check warning on line 373 in solidity/test/unit/modules/resolution/ERC20ResolutionModule.t.sol

View workflow job for this annotation

GitHub Actions / Run Linters (16.x)

Variable "_disputeId" is unused
module.forTest_setStartTime(_getId(mockDispute), block.timestamp);

// Expect an error to be thrown
vm.expectRevert(IERC20ResolutionModule.ERC20ResolutionModule_OnGoingVotingPhase.selector);

// Claim the refund
vm.prank(_voter);
module.claimVote(mockRequest, mockDispute);
}

/**
* @notice Releases the funds
*/
function test_releasesFunds(address _voter, uint256 _amount) public {
mockRequest.resolutionModuleData = abi.encode(
IERC20ResolutionModule.RequestParameters({
accountingExtension: accountingExtension,
votingToken: token,
minVotesForQuorum: 1,
timeUntilDeadline: 1
})
);

// Prepare the dispute
mockDispute.requestId = _getId(mockRequest);
module.forTest_setStartTime(_getId(mockDispute), block.timestamp);
module.forTest_setVotes(_getId(mockDispute), _voter, _amount);

// Expect the bond to be released
_mockAndExpect(
address(accountingExtension),
abi.encodeCall(accountingExtension.release, (_voter, mockDispute.requestId, token, _amount)),
abi.encode()
);

vm.warp(block.timestamp + 1000);

bytes32 _disputeId = _getId(mockDispute);
module.forTest_setVotes(_disputeId, _voter, _amount);

// Expect the event to be emitted
_expectEmit(address(module));
emit VoteClaimed(_voter, _disputeId, _amount);

// Claim the refund
vm.prank(_voter);
module.claimVote(mockRequest, mockDispute);
}
}

contract ERC20ResolutionModule_Unit_GetVoters is BaseTest {
/**
* @notice Test that `getVoters` returns an array of addresses of users that have voted.
Expand Down
20 changes: 20 additions & 0 deletions solidity/test/utils/Helpers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,24 @@ contract Helpers is DSTestPlus, TestConstants {
function _getId(IOracle.Dispute memory _dispute) internal pure returns (bytes32 _id) {
_id = keccak256(abi.encode(_dispute));
}

/**
* @notice Creates a mock contract, labels it and erases the bytecode
*
* @param _label The label to use for the mock contract
* @return _contract The address of the mock contract
*/
function _mockContract(string memory _label) internal returns (address _contract) {
_contract = makeAddr(_label);
vm.etch(_contract, hex'69');
}

/**
* @notice Sets an expectation for an event to be emitted
*
* @param _contract The contract to expect the event on
*/
function _expectEmit(address _contract) internal {
vm.expectEmit(true, true, true, true, _contract);
}
}

0 comments on commit db865ab

Please sign in to comment.