Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion solidity/contracts/Constants.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNICENSE
// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.33;

library Constants {
Expand Down
28 changes: 22 additions & 6 deletions solidity/contracts/ReputationToken.sol
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
// SPDX-License-Identifier: UNICENSE
// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.33;

import './ERC20.sol';

contract ReputationToken is ERC20 {

uint256 private totalTheoreticalSupply;
address public immutable zoltar;
event Mint(address account, uint256 value);
event Burn(address account, uint256 value);

modifier isZoltar {
require(msg.sender == zoltar, 'Not zoltar');
_;
}

constructor(address _zoltar) ERC20('Reputation', 'REP') {
zoltar = _zoltar;
}

function mint(address account, uint256 value) external {
require(msg.sender == zoltar, "Not zoltar");
function setMaxTheoreticalSupply(uint256 _totalTheoreticalSupply) external isZoltar {
totalTheoreticalSupply = _totalTheoreticalSupply;
}

function mint(address account, uint256 value) external isZoltar {
_mint(account, value);
emit Mint(account, value);
}

function burn(address account, uint256 value) external {
require(msg.sender == zoltar, "Not zoltar");
function burn(address account, uint256 value) external isZoltar {
_burn(account, value);
totalTheoreticalSupply -= value;
emit Burn(account, value);
}

function getTotalTheoreticalSupply() external view returns (uint256) {
return totalTheoreticalSupply;
}
}
265 changes: 98 additions & 167 deletions solidity/contracts/Zoltar.sol
Original file line number Diff line number Diff line change
@@ -1,207 +1,138 @@
// SPDX-License-Identifier: UNICENSE
// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.33;

import './Constants.sol';
import './ReputationToken.sol';

contract Zoltar {
uint256 constant FORK_THRESHOLD_DIVISOR = 20; // TODO, revisit, 5% of total supply atm
uint256 constant FORK_BURN_DIVISOR = 5; // TODO, revisit, 20% of fork threshold

contract Zoltar {
struct Universe {
ReputationToken reputationToken;
uint56 forkingQuestion;
uint256 forkTime;
ReputationToken reputationToken;
uint248 parentUniverseId;
uint8 forkingOutcomeIndex;
}

mapping(uint192 => Universe) public universes;

struct QuestionData {
uint64 endTime;
uint192 originUniverse;
address designatedReporter;
string extraInfo;
}

struct QuestionResolutionData {
address initialReporter;
Outcome outcome;
uint64 reportTime;
bool finalized;
}
mapping(uint248 => Universe) public universes;

enum Outcome {
Invalid,
Yes,
No,
None
struct UniverseForkData {
string forkingQuestionExtraInfo;
address forkedBy;
uint256 forkerRepDeposit;
string[4] forkingQuestionCategories;
}

mapping(uint56 => QuestionData) public questions;

// UniverseId => QuestionId => Data
mapping(uint192 => mapping(uint56 => QuestionResolutionData)) questionResolutions;

uint56 questionIdCounter = 0;

// TODO: Revist what behavior the bond should be
uint256 constant public REP_BOND = 1 ether;
mapping(uint248 => UniverseForkData) public universeForkData;

uint256 constant public DESIGNATED_REPORTING_TIME = 3 days;
uint256 constant public DISPUTE_PERIOD = 1 days;
event UniverseForked(address forker, uint248 universeId, string extraInfo, string[4] questionCategories);
event DeployChild(address deployer, uint248 universeId, uint8 outcomeIndex, uint248 childUniverseId, ReputationToken childReputationToken);
event SplitRep(uint248 universeId, uint256 amount, address migrator, address recipient, uint8[] outcomeIndexes);
event ForkerClaimRep(address forker, uint248 universeId, uint8[] outcomeIndexes, uint256 forkerRepDeposit);

constructor() {
universes[0] = Universe(
ReputationToken(Constants.GENESIS_REPUTATION_TOKEN),
0,
0
);
}

function isQuestionLegit(uint192 _universeId, uint56 _questionId) public view returns (bool) {
QuestionData memory questionData = questions[_questionId];
require(questionData.endTime != 0, "Question is not valid");

if (questionData.originUniverse == _universeId) return true;

Universe memory universeData = universes[_universeId];
require(address(universeData.reputationToken) != address(0), "Universe is not valid");

do {
_universeId >>= 2;
// If a parent didn't fork this wouldn't be a valid universe
Universe memory curUniverseData = universes[_universeId];
if (curUniverseData.forkTime == 0) return false;

// A resolved question cannot have children, as a question in a forked universe does not get resolved there
QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId];
if (questionResolutionDataIsFinalized(questionResolutionData)) return false;

// If other checks passed and the ids are equal its a legitimate child. If this never gets reached it isn't.
if (questionData.originUniverse == _universeId) return true;
} while (_universeId > 0);

return false;
}

function createQuestion(uint192 _universeId, uint64 _endTime, address _designatedReporterAddress, string memory _extraInfo) public returns (uint56 _questionId) {
Universe memory universe = universes[_universeId];
require(universe.forkingQuestion == 0, "Universe is forked");
universe.reputationToken.transferFrom(msg.sender, address(this), REP_BOND);
_questionId = ++questionIdCounter;
questions[_questionId] = QuestionData(
_endTime,
_universeId,
_designatedReporterAddress,
_extraInfo
);
}

function reportOutcome(uint192 _universeId, uint56 _questionId, Outcome _outcome) external {
Universe memory universe = universes[_universeId];
require(universe.forkingQuestion == 0, "Universe is forked");
QuestionData memory questionData = questions[_questionId];
QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId];
require(questionResolutionData.reportTime == 0, "Question already has a report");
require(_outcome != Outcome.None, "Invalid outcome");
require(block.timestamp > questionData.endTime, "Question has not ended");
require(msg.sender == questionData.designatedReporter || block.timestamp > questionData.endTime + DESIGNATED_REPORTING_TIME, "Reporter must be designated reporter");

questionResolutions[_universeId][_questionId].initialReporter = msg.sender;
questionResolutions[_universeId][_questionId].outcome = _outcome;
questionResolutions[_universeId][_questionId].reportTime = uint64(block.timestamp);
}

function finalizeQuestion(uint192 _universeId, uint56 _questionId) external returns (Outcome) {
Universe memory universe = universes[_universeId];
QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId];
if (!questionResolutionData.finalized) {
require(questionResolutionDataIsFinalized(questionResolutionData), "Cannot withdraw REP bond before finalized");
questionResolutionData.finalized = true;
questionResolutions[_universeId][_questionId] = questionResolutionData;
universe.reputationToken.transfer(questionResolutionData.initialReporter, REP_BOND);
}
return questionResolutionData.outcome;
function getForkTime(uint248 universeId) external view returns (uint256) {
Universe memory universe = universes[universeId];
return universe.forkTime;
}

function splitStakedRep(uint192 _universeId, uint56 _questionId) external {
QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId];
require(questionResolutionData.reportTime != 0, "No REP staked in this question");
require(!questionResolutionDataIsFinalized(questionResolutionData), "Cannot migrate REP from finalized question");

splitRepInternal(_universeId, REP_BOND, address(this), questionResolutionData.initialReporter, Outcome.None);
function getForkingQuestionCategories(uint248 universeId) external view returns (string[4] memory) {
return universeForkData[universeId].forkingQuestionCategories;
}

function isFinalized(uint192 _universeId, uint56 _questionId) external view returns (bool) {
QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId];
if (questionResolutionData.finalized) return true;
return questionResolutionDataIsFinalized(questionResolutionData);
function getRepToken(uint248 universeId) external view returns (ReputationToken) {
Universe memory universe = universes[universeId];
return universe.reputationToken;
}

function questionResolutionDataIsFinalized(QuestionResolutionData memory questionResolutionData) internal view returns (bool) {
return questionResolutionData.reportTime != 0 && block.timestamp > questionResolutionData.reportTime + DISPUTE_PERIOD;
function getForkedBy(uint248 universeId) external view returns (address) {
UniverseForkData memory forkData = universeForkData[universeId];
return forkData.forkedBy;
}

function getWinningOutcome(uint192 _universeId, uint56 _questionId) public view returns (Outcome) {
QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId];
require(questionResolutionDataIsFinalized(questionResolutionData), "Question is not finalized");

return questionResolutionData.outcome;
function getForkerDeposit(uint248 universeId) external view returns (uint256) {
UniverseForkData memory forkData = universeForkData[universeId];
return forkData.forkerRepDeposit;
}

// TODO: Currently escalation game is a single dispute. Likely will be more complex.
function dispute(uint192 _universeId, uint56 _questionId, Outcome _outcome) external {
Universe memory universe = universes[_universeId];
require(universe.forkingQuestion == 0, "Universe is forked");
QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId];
require(_outcome != questionResolutionData.outcome, "Dispute must be for a different outcome than the currently winning one");
require(block.timestamp < questionResolutionData.reportTime + DISPUTE_PERIOD, "Question not in dispute window");
require(_outcome != Outcome.None, "Invalid outcome");

uint256 disputeStake = REP_BOND * 2;

for (uint8 i = 1; i < Constants.NUM_OUTCOMES + 1; i++) {
uint192 childUniverseId = (_universeId << 2) + i;
universes[childUniverseId] = Universe(new ReputationToken{ salt: bytes32(uint256(childUniverseId)) }(address(this)), 0, 0);

questionResolutions[childUniverseId][_questionId].reportTime = 1;
questionResolutions[childUniverseId][_questionId].outcome = Outcome(i - 1);
questionResolutions[childUniverseId][_questionId].finalized = true;
}
constructor() {
universes[0] = Universe(0, ReputationToken(Constants.GENESIS_REPUTATION_TOKEN), 0, 0);
}

universe.forkingQuestion = _questionId;
universe.forkTime = block.timestamp;
universes[_universeId] = universe;
function getForkThreshold(uint248 universeId) public view returns (uint256) {
Universe memory universe = universes[universeId];
return universe.reputationToken.getTotalTheoreticalSupply() / FORK_THRESHOLD_DIVISOR;
}

splitRepInternal(_universeId, REP_BOND, questionResolutionData.initialReporter, questionResolutionData.initialReporter, questionResolutionData.outcome);
splitRepInternal(_universeId, disputeStake, msg.sender, msg.sender, _outcome);
function forkUniverse(uint248 universeId, string memory _extraInfo, string[4] memory _questionCategories) public {
Universe memory universe = universes[universeId];
require(universe.forkTime == 0, 'Universe has forked already');
require(_questionCategories.length >= 1, 'need atleast one category on top of invalid');
universes[universeId].forkTime = block.timestamp;
uint256 forkThreshold = getForkThreshold(universeId);
universeForkData[universeId] = UniverseForkData(_extraInfo, msg.sender, forkThreshold - forkThreshold / FORK_BURN_DIVISOR, _questionCategories);
universes[universeId].reputationToken.transferFrom(msg.sender, address(this), forkThreshold);
burnRep(universes[universeId].reputationToken, address(this), forkThreshold / FORK_BURN_DIVISOR); // burn 20%
emit UniverseForked(msg.sender, universeId, _extraInfo, _questionCategories);
}

function splitRep(uint192 universeId) public {
function splitRep(uint248 universeId, uint8[] memory outcomeIndexes) public {
uint256 amount = universes[universeId].reputationToken.balanceOf(msg.sender);
splitRepInternal(universeId, amount, msg.sender, msg.sender, Outcome.None);
splitRepInternal(universeId, amount, msg.sender, msg.sender, outcomeIndexes);
}

// singleOutcome will only credit the provided outcome if it is a valid outcome, else all child universe REP will be minted
function splitRepInternal(uint192 universeId, uint256 amount, address migrator, address recipient, Outcome singleOutcome) private {
Universe memory universe = universes[universeId];
require(universe.forkTime != 0, "Universe has not forked");

function burnRep(ReputationToken reputationToken, address migrator, uint256 amount) private {
// Genesis is using REPv2 which we cannot actually burn
if (universeId == 0) {
if (address(reputationToken) == Constants.GENESIS_REPUTATION_TOKEN) {
if (migrator == address(this)) {
universe.reputationToken.transfer(Constants.BURN_ADDRESS, amount);
reputationToken.transfer(Constants.BURN_ADDRESS, amount);
} else {
universe.reputationToken.transferFrom(migrator, Constants.BURN_ADDRESS, amount);
reputationToken.transferFrom(migrator, Constants.BURN_ADDRESS, amount);
}
} else {
ReputationToken(address(universe.reputationToken)).burn(migrator, amount);
ReputationToken(address(reputationToken)).burn(migrator, amount);
}
}

for (uint8 i = 1; i < Constants.NUM_OUTCOMES + 1; i++) {
if (singleOutcome != Outcome.None && i != uint8(singleOutcome) + 1) continue;
uint192 childUniverseId = (universeId << 2) + i;
Universe memory childUniverse = universes[childUniverseId];
ReputationToken(address(childUniverse.reputationToken)).mint(recipient, amount);
}
function getOutcomeName(uint248 universeId) external view returns (string memory) {
if (universeId == 0) return 'Genesis';
Universe memory universe = universes[universeId];
if (universe.forkingOutcomeIndex == 0) return 'Invalid';
return universeForkData[universe.parentUniverseId].forkingQuestionCategories[universe.forkingOutcomeIndex - 1];
}

function getChildUniverseId(uint248 universeId, uint8 outcomeIndex) public pure returns (uint248) {
return uint248(uint256(keccak256(abi.encode(universeId, outcomeIndex))));
}

function deployChild(uint248 universeId, uint8 outcomeIndex) public {
Universe memory universe = universes[universeId];
require(universe.forkTime != 0, 'Universe has not forked');
uint248 childUniverseId = getChildUniverseId(universeId, outcomeIndex);
ReputationToken childReputationToken = new ReputationToken{ salt: bytes32(uint256(childUniverseId)) }(address(this));
childReputationToken.setMaxTheoreticalSupply(universe.reputationToken.getTotalTheoreticalSupply());
universes[childUniverseId] = Universe(0, childReputationToken, universeId, outcomeIndex);
emit DeployChild(msg.sender, universeId, outcomeIndex, childUniverseId, childReputationToken);
}

function forkerClaimRep(uint248 universeId, uint8[] memory outcomeIndexes) public {
UniverseForkData memory data = universeForkData[universeId];
require(data.forkedBy == msg.sender, 'only forker can claim');
universeForkData[universeId].forkerRepDeposit = 0;
emit ForkerClaimRep(msg.sender, universeId, outcomeIndexes, data.forkerRepDeposit);
splitRepInternal(universeId, data.forkerRepDeposit, address(this), data.forkedBy, outcomeIndexes);
}

function splitRepInternal(uint248 universeId, uint256 amount, address migrator, address recipient, uint8[] memory outcomeIndexes) private {
Universe memory universe = universes[universeId];
require(universe.forkTime != 0, 'Universe has not forked');
emit SplitRep(universeId, amount, migrator, recipient, outcomeIndexes);
burnRep(universe.reputationToken, migrator, amount);
for (uint8 i = 0; i < outcomeIndexes.length; i++) {
require(i == 0 || outcomeIndexes[i] > outcomeIndexes[i - 1], 'outcomes are not sorted'); // force sorting to avoid duplicate indices
require(outcomeIndexes[i] < universeForkData[universeId].forkingQuestionCategories.length + 1, 'outcome index overflow');
uint248 childUniverseId = getChildUniverseId(universeId, outcomeIndexes[i]);
if (address(universes[childUniverseId].reputationToken) == address(0x0)) deployChild(universeId, outcomeIndexes[i]);
universes[childUniverseId].reputationToken.mint(recipient, amount);
}
}
}

Loading