Skip to content

Commit

Permalink
feat: adding erc20 resolution module (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xng authored Jun 21, 2023
1 parent e527edb commit bccc9ff
Show file tree
Hide file tree
Showing 2 changed files with 229 additions and 0 deletions.
175 changes: 175 additions & 0 deletions solidity/contracts/modules/ERC20ResolutonModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';

import {IERC20ResolutionModule} from '../../interfaces/modules/IERC20ResolutionModule.sol';
import {IOracle} from '../../interfaces/IOracle.sol';
import {IDisputeModule} from '../../interfaces/modules/IDisputeModule.sol';
import {IAccountingExtension} from '../../interfaces/extensions/IAccountingExtension.sol';
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';

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

contract ERC20ResolutionModule is Module, IERC20ResolutionModule {
using SafeERC20 for IERC20;

uint256 public constant BASE = 100;

mapping(bytes32 _disputeId => EscalationData _escalationData) public escalationData;
mapping(bytes32 _disputeId => VoterData[]) public votes;
mapping(bytes32 _disputeId => uint256 _numOfVotes) public totalNumberOfVotes;

constructor(IOracle _oracle) Module(_oracle) {}

function moduleName() external pure returns (string memory _moduleName) {
return 'ERC20ResolutionModule';
}

function decodeRequestData(bytes32 _requestId)
public
view
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 _timeUntilDeadline
)
{
(_accountingExtension, _token, _disputerBondSize, _minQuorum, _timeUntilDeadline) =
abi.decode(_data, (IAccountingExtension, IERC20, uint256, uint256, uint256));
}

function escalateDispute(bytes32 _disputeId) external {
bytes32 _requestId = ORACLE.getDispute(_disputeId).requestId;
IDisputeModule _disputeModule = ORACLE.getRequest(_requestId).disputeModule;
if (msg.sender != address(_disputeModule)) revert ERC20ResolutionModule_OnlyDisputeModule();
(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);
}

// casts vote in favor of dispute
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();

EscalationData memory _escalationData = escalationData[_disputeId];

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

(, 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}));

escalationData[_disputeId].totalVotes += _numberOfVotes;

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

function resolveDispute(bytes32 _disputeId) external {
/*
TODO: check caller?
*/

// 0. Check that the disputeId actually exists
IOracle.Dispute memory _dispute = ORACLE.getDispute(_disputeId);
if (_dispute.createdAt == 0) revert ERC20ResolutionModule_NonExistentDispute();

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);
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;

VoterData[] memory _voterData = votes[_disputeId];

uint256 _disputerBond = _escalationData.disputerBond;
uint256 _amountToPay;
// 5. Pay and Release
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;
}
}
} 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;
}
}
}

if (_disputerBond != 0) {
escalationData[_disputeId].disputerBond = 0;
}

emit DisputeResolved(_disputeId);
}
}
54 changes: 54 additions & 0 deletions solidity/interfaces/modules/IERC20ResolutionModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.16 <0.9.0;

//TODO: add getters

import {IOracle} from '../IOracle.sol';
import {IResolutionModule} from './IResolutionModule.sol';
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {IAccountingExtension} from '../extensions/IAccountingExtension.sol';

interface IERC20ResolutionModule is IResolutionModule {
struct EscalationData {
uint128 startTime;
uint128 results; // 0 = Escalated, 1 = Disputer Won, 2 = Disputer Lost
uint256 disputerBond;
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);

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

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 castVote(bytes32 _requestId, bytes32 _disputeId, uint256 _numberOfVotes) external;
function resolveDispute(bytes32 _disputeId) external;
function decodeRequestData(bytes32 _requestId)
external
view
returns (
IAccountingExtension _accountingExtension,
IERC20 _token,
uint256 _disputerBondSize,
uint256 _minQuorum,
uint256 _timeUntilDeadline
);
}

0 comments on commit bccc9ff

Please sign in to comment.