Skip to content

Commit

Permalink
refactor: initial refactor of erc20module (#50)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes OPO-162
  • Loading branch information
0xng authored Jul 26, 2023
2 parents 1313692 + 17648cd commit 9349efb
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,11 @@ contract BondEscalationResolutionModule is Module, IBondEscalationResolutionModu
}

uint256 _currentTotalVotes = _currentForVotes + _currentAgainstVotes;

// TODO: add larger coefficient
uint256 _currentForVotesPercentage = _updatedForVotes * 100 / _currentTotalVotes;
uint256 _currentAgainstVotesPercentage = _currentAgainstVotes * 100 / _currentTotalVotes;

// TODO: check math
int256 _forPercentageDifference = int256(_currentForVotesPercentage) - int256(_currentAgainstVotesPercentage);
int256 _againstPercentageDifference = int256(_currentAgainstVotesPercentage) - int256(_currentForVotesPercentage);
Expand Down Expand Up @@ -318,7 +320,8 @@ contract BondEscalationResolutionModule is Module, IBondEscalationResolutionModu
uint256 _pledgingDeadline = _escalationData.startTime + _timeUntilDeadline;

// Revert if we have not yet reached the deadline and the timer has not passed
// TODO: double check this when fresh
// TODO: double check this when fresh - This is wrong because _inequalityTimerDeadline may never be 0, as that would require _timeToBreakInequality to be 0
// the actual check should be something along the lines of _inequalityData.time != 0 not _inequalityTimerDeadline. check though
if (
block.timestamp < _pledgingDeadline && _inequalityTimerDeadline != 0 && block.timestamp < _inequalityTimerDeadline
) revert BondEscalationResolutionModule_PledgingPhaseNotOver();
Expand Down
1 change: 1 addition & 0 deletions solidity/contracts/modules/BondedDisputeModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ contract BondedDisputeModule is Module, IBondedDisputeModule {
_accountingExtension.bond(_disputer, _requestId, _bondToken, _bondSize);
}

// TODO: This doesn't handle the cases of unconclusive statuses
function updateDisputeStatus(bytes32, /* _disputeId */ IOracle.Dispute memory _dispute) external onlyOracle {
(IAccountingExtension _accountingExtension, IERC20 _bondToken, uint256 _bondSize) =
_decodeRequestData(requestData[_dispute.requestId]);
Expand Down
131 changes: 39 additions & 92 deletions solidity/contracts/modules/ERC20ResolutionModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ import {IERC20ResolutionModule} from '../../interfaces/modules/IERC20ResolutionM
import {IOracle} from '../../interfaces/IOracle.sol';
import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol';
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import {EnumerableSet} from '@openzeppelin/contracts/utils/structs/EnumerableSet.sol';

import {Module} from '../Module.sol';

// TODO: Discuss about this module's incentives for voters. Right now there are no incentives for them to vote. There's the possibility of adding the bonded amount of the disputer/proposer as rewards
// but that would get highly diluted - and due to the nature of how updateDisputeStatus work, this would need a custom dispute module that doesn't settle payment between proposer and disputer
// as this would all get handled in this module.
contract ERC20ResolutionModule is Module, IERC20ResolutionModule {
using SafeERC20 for IERC20;

uint256 public constant BASE = 100;
using EnumerableSet for EnumerableSet.AddressSet;

mapping(bytes32 _disputeId => EscalationData _escalationData) public escalationData;
mapping(bytes32 _disputeId => VoterData[]) public votes;
mapping(bytes32 _disputeId => uint256 _numOfVotes) public totalNumberOfVotes;
mapping(bytes32 _disputeId => mapping(address _voter => uint256 _numOfVotes)) public votes;
mapping(bytes32 _disputeId => EnumerableSet.AddressSet _votersSet) private _voters;

constructor(IOracle _oracle) Module(_oracle) {}

Expand All @@ -31,138 +34,82 @@ contract ERC20ResolutionModule is Module, IERC20ResolutionModule {
returns (
IAccountingExtension _accountingExtension,
IERC20 _token,
uint256 _disputerBondSize,
uint256 _minQuorum,
uint256 _timeUntilDeadline
)
{
(_accountingExtension, _token, _disputerBondSize, _minQuorum, _timeUntilDeadline) =
_decodeRequestData(requestData[_requestId]);
}

function _decodeRequestData(bytes memory _data)
internal
pure
returns (
IAccountingExtension _accountingExtension,
IERC20 _token,
uint256 _disputerBondSize,
uint256 _minQuorum,
uint256 _minVotesForQuorum,
uint256 _timeUntilDeadline
)
{
(_accountingExtension, _token, _disputerBondSize, _minQuorum, _timeUntilDeadline) =
abi.decode(_data, (IAccountingExtension, IERC20, uint256, uint256, uint256));
(_accountingExtension, _token, _minVotesForQuorum, _timeUntilDeadline) =
abi.decode(requestData[_requestId], (IAccountingExtension, IERC20, uint256, uint256));
}

function startResolution(bytes32 _disputeId) external onlyOracle {
bytes32 _requestId = ORACLE.getDispute(_disputeId).requestId;
(IAccountingExtension _accounting, IERC20 _token, uint256 _disputerBondSize,,) = decodeRequestData(_requestId);

escalationData[_disputeId].startTime = uint128(block.timestamp);

IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId);

if (_disputerBondSize != 0) {
// seize disputer bond until resolution - this allows for voters not having to call deposit in the accounting extension
// TODO: should another event be emitted with disputerBond?
_accounting.pay(_requestId, _dispute.disputer, address(this), _token, _disputerBondSize);
_accounting.withdraw(_token, _disputerBondSize);
escalationData[_disputeId].disputerBond = _disputerBondSize;
}

emit VotingPhaseStarted(uint128(block.timestamp), _disputeId);
escalationData[_disputeId].startTime = block.timestamp;
emit VotingPhaseStarted(block.timestamp, _disputeId);
}

// casts vote in favor of dispute
// Casts vote in favor of dispute
// TODO: Discuss whether to change this to vote against disputes/for disputes
function castVote(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes) public {
/*
1. Check that the disputeId is Escalated - TODO
2. Check that the voting deadline is not over
3. Transfer tokens from msg.sender to this address and increase the mapping
4. Emit VoteCast event
*/
IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId);
if (_dispute.createdAt == 0) revert ERC20ResolutionModule_NonExistentDispute();
if (_dispute.status != IOracle.DisputeStatus.None) revert ERC20ResolutionModule_AlreadyResolved();

EscalationData memory _escalationData = escalationData[_disputeId];

if (_escalationData.startTime == 0) revert ERC20ResolutionModule_DisputeNotEscalated();

(, IERC20 _token,,, uint256 _timeUntilDeadline) = decodeRequestData(_requestId);
(, IERC20 _token,, uint256 _timeUntilDeadline) = decodeRequestData(_requestId);
uint256 _deadline = _escalationData.startTime + _timeUntilDeadline;
if (block.timestamp >= _deadline) revert ERC20ResolutionModule_VotingPhaseOver();

// TODO: create an enumerable set-like structure where the index is the address
// otherwise if new members are pushed, they can vote with 1 wei for example
// and DoS the loading of the array
votes[_disputeId].push(VoterData({voter: msg.sender, numOfVotes: _numberOfVotes}));
votes[_disputeId][msg.sender] += _numberOfVotes;

_voters[_disputeId].add(msg.sender);
escalationData[_disputeId].totalVotes += _numberOfVotes;

_token.safeTransferFrom(msg.sender, address(this), _numberOfVotes);
emit VoteCast(msg.sender, _disputeId, _numberOfVotes);
}

function resolveDispute(bytes32 _disputeId) external onlyOracle {
// 0. Check that the disputeId actually exists
// 0. Check disputeId actually exists and that it isnt 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();

EscalationData memory _escalationData = escalationData[_disputeId];

// Check that the dispute is actually escalated
if (_escalationData.startTime == 0) revert ERC20ResolutionModule_DisputeNotEscalated();

// 2. Check that voting deadline is over
(IAccountingExtension _accounting, IERC20 _token,, uint256 _minQuorum, uint256 _timeUntilDeadline) =
decodeRequestData(_dispute.requestId);
(, IERC20 _token, uint256 _minVotesForQuorum, uint256 _timeUntilDeadline) = decodeRequestData(_dispute.requestId);
uint256 _deadline = _escalationData.startTime + _timeUntilDeadline;
if (block.timestamp < _deadline) revert ERC20ResolutionModule_OnGoingVotingPhase();

// 3. Check quorum - TODO: check if this is precise- think if using totalSupply makes sense, perhaps minQuorum can be
// min amount of tokens required instead of a percentage
// not sure if safe but the actual formula is _token.totalSupply() * _minQuorum * BASE(100) / 100 so base disappears
// i guess with a shit token someone could front run this call and increase totalSupply enough for this to fail
uint256 _numVotesForQuorum = _token.totalSupply() * _minQuorum;
uint256 _quorumReached = _escalationData.totalVotes * BASE >= _numVotesForQuorum ? 1 : 0;

// 4. Store result
escalationData[_disputeId].results = _quorumReached == 1 ? 1 : 2;
uint256 _quorumReached = _escalationData.totalVotes >= _minVotesForQuorum ? 1 : 0;

VoterData[] memory _voterData = votes[_disputeId];
address[] memory __voters = _voters[_disputeId].values();

uint256 _disputerBond = _escalationData.disputerBond;
uint256 _amountToPay;
// 5. Pay and Release
// 5. Update status
if (_quorumReached == 1) {
for (uint256 _i; _i < _voterData.length;) {
// TODO: check math -- remember _numVotesForQuorum is escalated
_amountToPay = _disputerBond == 0
? _voterData[_i].numOfVotes
: _voterData[_i].numOfVotes + (_voterData[_i].numOfVotes * _numVotesForQuorum / _disputerBond * BASE);
_token.safeTransfer(_voterData[_i].voter, _amountToPay);
unchecked {
++_i;
}
}
ORACLE.updateDisputeStatus(_disputeId, IOracle.DisputeStatus.Won);
emit DisputeResolved(_disputeId, IOracle.DisputeStatus.Won);
} else {
// This also releases the disputer's bond
if (_disputerBond != 0) {
_accounting.pay(_dispute.requestId, address(this), _dispute.disputer, _token, _disputerBond);
}
for (uint256 _i; _i < _voterData.length;) {
_token.safeTransfer(_voterData[_i].voter, _voterData[_i].numOfVotes);
unchecked {
++_i;
}
}
ORACLE.updateDisputeStatus(_disputeId, IOracle.DisputeStatus.Lost);
emit DisputeResolved(_disputeId, IOracle.DisputeStatus.Lost);
}

if (_disputerBond != 0) {
escalationData[_disputeId].disputerBond = 0;
uint256 _votersLength = __voters.length;

// 6. Return tokens
for (uint256 _i; _i < _votersLength;) {
_token.safeTransfer(__voters[_i], votes[_disputeId][__voters[_i]]);
unchecked {
++_i;
}
}
}

emit DisputeResolved(_disputeId);
function getVoters(bytes32 _disputeId) external view returns (address[] memory __voters) {
__voters = _voters[_disputeId].values();
}
}
28 changes: 9 additions & 19 deletions solidity/interfaces/modules/IERC20ResolutionModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,38 @@ pragma solidity >=0.8.16 <0.9.0;
import {IResolutionModule} from './IResolutionModule.sol';
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {IAccountingExtension} from '../extensions/IAccountingExtension.sol';
import {IOracle} from '../IOracle.sol';

interface IERC20ResolutionModule is IResolutionModule {
struct EscalationData {
uint128 startTime;
uint128 results; // 0 = Escalated, 1 = Disputer Won, 2 = Disputer Lost
uint256 disputerBond;
uint256 startTime;
uint256 totalVotes;
}

struct VoterData {
address voter;
uint256 numOfVotes;
}

event VoteCast(address _voter, bytes32 _disputeId, uint256 _numberOfVotes);
event VotingPhaseStarted(uint128 _startTime, bytes32 _disputeId);
event DisputeResolved(bytes32 _disputeId);
event VotingPhaseStarted(uint256 _startTime, bytes32 _disputeId);
event DisputeResolved(bytes32 _disputeId, IOracle.DisputeStatus _status);

error ERC20ResolutionModule_OnlyDisputeModule();
error ERC20ResolutionModule_DisputeNotEscalated();
error ERC20ResolutionModule_UnresolvedDispute();
error ERC20ResolutionModule_VotingPhaseOver();
error ERC20ResolutionModule_OnGoingVotingPhase();
error ERC20ResolutionModule_NonExistentDispute();
error ERC20ResolutionModule_AlreadyResolved();

function escalationData(bytes32 _disputeId)
external
view
returns (uint128 _startTime, uint128 _results, uint256 _disputerBond, uint256 _totalVotes);
// TODO: create getter -- see if its possible to declare this
// function votes(bytes32 _disputeId) external view returns (VoterData memory _voterData);
function totalNumberOfVotes(bytes32 _disputeId) external view returns (uint256 _numOfVotes);
function escalationData(bytes32 _disputeId) external view returns (uint256 _startTime, uint256 _totalVotes);
function castVote(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes) external;
function votes(bytes32 _disputeId, address _voter) external view returns (uint256 _votes);
function resolveDispute(bytes32 _disputeId) external;
function getVoters(bytes32 _disputeId) external view returns (address[] memory _voters);
function decodeRequestData(bytes32 _requestId)
external
view
returns (
IAccountingExtension _accountingExtension,
IERC20 _token,
uint256 _disputerBondSize,
uint256 _minQuorum,
uint256 _minVotesForQuorum,
uint256 _timeUntilDeadline
);
}

0 comments on commit 9349efb

Please sign in to comment.