Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ ETHERSCAN_API_KEY=ABC123ABC123ABC123ABC123ABC123ABC1
RINKEBY_URL=https://eth-rinkeby.alchemyapi.io/v2/wJsSSKTeAmqF2G5D56g00CX8fzwumcu0
GOERLI_URL=https://eth-goerli.alchemyapi.io/v2/bfUg0Rqe5lS1SeU7bE6PVoDDCOVlNr1P
PRIVATE_KEY=e5f86b5eaf2888e6b7e799f95be61da578b8629fa1bc4588cf202675e7787bd9
REPORT_GAS=true
REPORT_GAS=true

# Get an API key here if you want to see USD prices in test reports
# https://pro.coinmarketcap.com/signup
COINMARKETCAP_API_KEY=abcdefg-1234-5678-90ab-cdefghijklmn
207 changes: 207 additions & 0 deletions contracts/GoldenProtocol.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;

import '@openzeppelin/contracts/access/Ownable.sol';
import '@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol';

import './GoldenToken.sol';
import './libraries/AddressSet.sol';

/// @custom:security-contact [email protected]
contract GoldenProtocol is Ownable {
using SafeERC20Upgradeable for GoldenToken;
GoldenToken tokenContract;
uint256 public minimumVotes;

event QuestionCreated(
address indexed questionAddress,
bytes16 subjectUUID,
bytes16 predicateUUID
);

constructor(address goldenTokenAddress, uint256 _minimumVotes) Ownable() {
tokenContract = GoldenToken(goldenTokenAddress);
minimumVotes = _minimumVotes;
}

function setMinimumVotes(uint256 _minimumVotes) public onlyOwner {
minimumVotes = _minimumVotes;
}

function createQuestion(
bytes16 subjectUUID,
bytes16 predicateUUID,
uint256 bounty
) public returns (address) {
require(
tokenContract.allowance(_msgSender(), address(this)) >= bounty,
'GoldenProtocol: insufficient allowance'
);

// TODO: Creating new contracts is going to be very expensive.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can get this cost lower by using an l2 chain?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be a way to get it down. At current prices for this function on mainnet ETH is ~$30.

// Is there a cheaper alternative to encapsulate the question logic?
GoldenProtocolQuestion newQuestion = new GoldenProtocolQuestion(
address(tokenContract),
msg.sender,
subjectUUID,
predicateUUID
);
address newQuestionAddress = address(newQuestion);
tokenContract.safeTransferFrom(
_msgSender(),
newQuestionAddress,
bounty
);
emit QuestionCreated(newQuestionAddress, subjectUUID, predicateUUID);
return newQuestionAddress;
}
}

contract GoldenProtocolQuestion is Ownable {
using SafeERC20Upgradeable for GoldenToken;
GoldenToken tokenContract;

using AddressSet for AddressSet.Set;

address public asker;
bytes16 public subjectUUID;
bytes16 public predicateUUID;
string public answer;

AddressSet.Set answerers;
AddressSet.Set verifiers;

// Mapping of answerer address to their answer
mapping(address => string) answerByAnswerer;

// Mapping of voter address to the index of an answer
mapping(address => uint256) answerIndexByVerifier;

// Mapping of answerer address to their vote count (score)
mapping(address => uint256) voteCountByAnswerer;

// Helper struct for consensus/payout algorithms
struct Answer {
address answerer;
string answer;
uint256 voteCount;
}

event AnswerAdded(
bytes16 subjectUUID,
bytes16 predicateUUID,
string answer,
uint256 index
);

constructor(
address goldenTokenAddress,
address _asker,
bytes16 _subjectUUID,
bytes16 _predicateUUID
) Ownable() {
require(
_asker != address(0),
'GoldenProtocolQuestion: asker is the zero address'
);
tokenContract = GoldenToken(goldenTokenAddress);
asker = _asker;
subjectUUID = _subjectUUID;
predicateUUID = _predicateUUID;
}

modifier onlyAsker() {
require(msg.sender == asker, 'GoldenProtocolQuestion: onlyAsker');
_;
}

function bounty() public view returns (uint256) {
return tokenContract.balanceOf(address(this));
}

function addAnswer(string calldata _answer) public {
require(
bytes(_answer).length > 0,
'GoldenProtocolQuestion: answer is empty'
);

address answerer = msg.sender;
answerByAnswerer[answerer] = _answer;
answerers.upsert(answerer);
voteCountByAnswerer[answerer] = 0;
emit AnswerAdded(
subjectUUID,
predicateUUID,
_answer,
answerers.indexOfKey(answerer)
);
}

function answers() public view returns (Answer[] memory) {
Answer[] memory _answers = new Answer[](answerers.count());
for (uint256 i = 0; i < answerers.count(); i++) {
address answerer = answerers.keyAtIndex(i);
_answers[i] = Answer(
answerer,
answerByAnswerer[answerer],
voteCountByAnswerer[answerer]
);
}
return _answers;
}

function upvote(uint256 index) public {
require(
answerers.count() > index,
'GoldenProtocolQuestion: there is no answer at that index'
);
require(
answerIndexByVerifier[msg.sender] == 0,
'GoldenProtocolQuestion: you have already voted'
);

address answerer = answerers.keyAtIndex(index);
address verifier = msg.sender;
voteCountByAnswerer[answerer] += 1;
answerIndexByVerifier[verifier] = index;
verifiers.upsert(verifier);
}

function topAnswer() public view returns (Answer memory) {
uint256 maxVotes = 0;
uint256 maxVotesIndex = 0;
for (uint256 i = 0; i < answerers.count(); i++) {
uint256 voteCount = voteCountByAnswerer[answerers.keyAtIndex(i)];
if (voteCount >= maxVotes) {
maxVotes = voteCount;
maxVotesIndex = i;
}
}
address answerer = answerers.keyAtIndex(maxVotesIndex);
return (Answer(answerer, answerByAnswerer[answerer], maxVotes));
}

function payout() public onlyAsker {
Answer memory _topAnswer = topAnswer();
require(
GoldenProtocol(owner()).minimumVotes() <= _topAnswer.voteCount,
'GoldenProtocolQuestion: payout: minimumVotes not met'
);
answer = _topAnswer.answer;
address payable answerer = payable(_topAnswer.answerer);
tokenContract.safeTransfer(
answerer,
// TODO: Skim a small fee for the voters and protocol
tokenContract.balanceOf(address(this))
);
}

// Utils
function hashAnswer(address answerer, string memory value)
public
pure
returns (uint256)
{
return uint256(keccak256(abi.encode(answerer, value)));
}
}
14 changes: 7 additions & 7 deletions contracts/GoldenToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ contract GoldenToken is
) internal override {
super._beforeTokenTransfer(from, to, amount);

require(
(from == address(0) ||
from == owner() ||
from == address(this) ||
to == address(this)),
'ERC20: Not allowed to transfer'
);
// require(
// (from == address(0) ||
// from == owner() ||
// from == address(this) ||
// to == address(this)),
// 'ERC20: Not allowed to transfer'
// );
}

// ============ Staking ============
Expand Down
69 changes: 69 additions & 0 deletions contracts/libraries/AddressSet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: Unlicensed
pragma solidity ^0.8.16;

// Based on: https://github.com/rob-Hitchens/UnorderedKeySet/blob/master/contracts/HitchensUnorderedAddressSet.sol

library AddressSet {
struct Set {
mapping(address => uint256) keyPointers;
address[] keyList;
}

function insert(Set storage self, address key) internal {
require(key != address(0), 'UnorderedKeySet(100) - Key cannot be 0x0');
require(
!exists(self, key),
'UnorderedAddressSet(101) - Address (key) already exists in the set.'
);
self.keyList.push(key);
self.keyPointers[key] = self.keyList.length - 1;
}

function upsert(Set storage self, address key) internal {
if (!exists(self, key)) {
insert(self, key);
}
}

function remove(Set storage self, address key) internal {
require(
exists(self, key),
'UnorderedKeySet(102) - Address (key) does not exist in the set.'
);
address keyToMove = self.keyList[count(self) - 1];
uint256 rowToReplace = self.keyPointers[key];
self.keyPointers[keyToMove] = rowToReplace;
self.keyList[rowToReplace] = keyToMove;
delete self.keyPointers[key];
self.keyList.pop();
}

function count(Set storage self) internal view returns (uint256) {
return (self.keyList.length);
}

function exists(Set storage self, address key)
internal
view
returns (bool)
{
if (self.keyList.length == 0) return false;
return self.keyList[self.keyPointers[key]] == key;
}

function keyAtIndex(Set storage self, uint256 index)
internal
view
returns (address)
{
return self.keyList[index];
}

function indexOfKey(Set storage self, address key)
internal
view
returns (uint256)
{
return self.keyPointers[key];
}
}
23 changes: 23 additions & 0 deletions deploy/999_GoldenProtocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { HardhatRuntimeEnvironment } from 'hardhat/types';
import { DeployFunction } from 'hardhat-deploy/types';

const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployments, getNamedAccounts } = hre;

const { deployer } = await getNamedAccounts();

const GoldenTokenDeployment = await deployments.get('GoldenToken');

await deployments.deploy('GoldenProtocol', {
from: deployer,
// skipIfAlreadyDeployed: true,
args: [GoldenTokenDeployment.address, 3],
log: true,
});
};

deploy.id = 'deploy_golden_protocol';
deploy.tags = ['GoldenProtocol'];
deploy.dependencies = ['GoldenToken'];

export default deploy;
1 change: 1 addition & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const config: HardhatUserConfig = {
},
gasReporter: {
enabled: process.env.REPORT_GAS !== undefined,
coinmarketcap: process.env.COINMARKETCAP_API_KEY,
currency: 'USD',
},
etherscan: {
Expand Down
Loading