Skip to content

Commit 83c069e

Browse files
committed
Rough first sketch of question/answer flow
1 parent 0c24ded commit 83c069e

File tree

16 files changed

+2015
-0
lines changed

16 files changed

+2015
-0
lines changed

contracts/GoldenProtocol.sol

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.16;
3+
4+
import '@openzeppelin/contracts/access/Ownable.sol';
5+
6+
import './libraries/AddressSet.sol';
7+
8+
/// @custom:security-contact security@golden.com
9+
contract GoldenProtocol is Ownable {
10+
using AddressSet for AddressSet.Set;
11+
// This is not going to scale well... how to keep track of questions without this?
12+
AddressSet.Set _questions;
13+
string[] entities;
14+
uint256 public minimumVotes;
15+
16+
constructor(uint256 _minimumVotes) Ownable() {
17+
minimumVotes = _minimumVotes;
18+
}
19+
20+
function setMinimumVotes(uint256 _minimumVotes) public onlyOwner {
21+
minimumVotes = _minimumVotes;
22+
}
23+
24+
function createQuestion(
25+
bytes16 subjectUUID,
26+
bytes16 predicateUUID,
27+
uint256 bounty
28+
) public returns (address) {
29+
// TODO: Creating new contracts is going to be very expensive.
30+
// Is there a cheaper alternative to encapsulate the question logic?
31+
GoldenProtocolQuestion newQuestion = new GoldenProtocolQuestion(
32+
address(this),
33+
msg.sender,
34+
subjectUUID,
35+
predicateUUID,
36+
bounty
37+
);
38+
address newQuestionAddress = address(newQuestion);
39+
_questions.insert(newQuestionAddress);
40+
return newQuestionAddress;
41+
}
42+
43+
// TODO: This is not going to scale well... how to keep track of questions without this?
44+
function questions() public view returns (address[] memory) {
45+
return _questions.keyList;
46+
}
47+
}
48+
49+
contract GoldenProtocolQuestion {
50+
using AddressSet for AddressSet.Set;
51+
52+
address public owner;
53+
address public asker;
54+
uint256 public bounty;
55+
bytes16 public subjectUUID;
56+
bytes16 public predicateUUID;
57+
string public answer;
58+
59+
AddressSet.Set answerers;
60+
AddressSet.Set verifiers;
61+
62+
// Mapping of answerer address to their answer
63+
mapping(address => string) answerByAnswerer;
64+
65+
// Mapping of voter address to the index of an answer
66+
mapping(address => uint256) answerIndexByVerifier;
67+
68+
// Mapping of answerer address to their vote count (score)
69+
mapping(address => uint256) voteCountByAnswerer;
70+
71+
// Helper struct for consensus/payout algorithms
72+
struct Answer {
73+
address answerer;
74+
string answer;
75+
uint256 voteCount;
76+
}
77+
78+
constructor(
79+
address _owner,
80+
address _asker,
81+
bytes16 _subjectUUID,
82+
bytes16 _predicateUUID,
83+
uint256 _bounty
84+
) {
85+
owner = _owner;
86+
asker = _asker;
87+
subjectUUID = _subjectUUID;
88+
predicateUUID = _predicateUUID;
89+
bounty = _bounty;
90+
}
91+
92+
modifier onlyAsker() {
93+
require(msg.sender == asker, 'GoldenProtocolQuestion: onlyAsker');
94+
_;
95+
}
96+
97+
function addAnswer(string calldata _answer) public {
98+
address answerer = msg.sender;
99+
answerByAnswerer[answerer] = _answer;
100+
answerers.upsert(answerer);
101+
voteCountByAnswerer[answerer] = 0;
102+
}
103+
104+
function answers() public view returns (string[] memory) {
105+
string[] memory _answers = new string[](answerers.count());
106+
for (uint256 i = 0; i < answerers.count(); i++) {
107+
_answers[i] = answerByAnswerer[answerers.keyAtIndex(i)];
108+
}
109+
return _answers;
110+
}
111+
112+
function upvote(uint256 index) public {
113+
require(
114+
answerers.count() > index,
115+
'GoldenProtocolQuestion: there is no answer at that index'
116+
);
117+
address answerer = answerers.keyAtIndex(index);
118+
address verifier = msg.sender;
119+
voteCountByAnswerer[answerer] += 1;
120+
answerIndexByVerifier[verifier] = index;
121+
verifiers.upsert(verifier);
122+
}
123+
124+
function votes() public view returns (uint256[] memory) {
125+
uint256[] memory _votes = new uint256[](answerers.count());
126+
for (uint256 i = 0; i < answerers.count(); i++) {
127+
_votes[i] = voteCountByAnswerer[answerers.keyAtIndex(i)];
128+
}
129+
return _votes;
130+
}
131+
132+
function topAnswer() public view returns (Answer memory) {
133+
uint256 maxVotes = 0;
134+
uint256 maxVotesIndex = 0;
135+
for (uint256 i = 0; i < answerers.count(); i++) {
136+
uint256 voteCount = voteCountByAnswerer[answerers.keyAtIndex(i)];
137+
if (voteCount >= maxVotes) {
138+
maxVotes = voteCount;
139+
maxVotesIndex = i;
140+
}
141+
}
142+
address answerer = answerers.keyAtIndex(maxVotesIndex);
143+
return (Answer(answerer, answerByAnswerer[answerer], maxVotes));
144+
}
145+
146+
// TODO: Pay out the bounty to the best answer, skim a small fee for the voters and protocol
147+
function payout() public onlyAsker {
148+
Answer memory _topAnswer = topAnswer();
149+
GoldenProtocol protocol = GoldenProtocol(owner);
150+
require(
151+
protocol.minimumVotes() <= _topAnswer.voteCount,
152+
'GoldenProtocolQuestion: payout: minimumVotes not met'
153+
);
154+
address payable answerer = payable(_topAnswer.answerer);
155+
answerer.transfer(bounty);
156+
}
157+
158+
// Utils
159+
function hashAnswer(address answerer, string memory value)
160+
public
161+
pure
162+
returns (uint256)
163+
{
164+
return uint256(keccak256(abi.encode(answerer, value)));
165+
}
166+
}

contracts/libraries/AddressSet.sol

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// SPDX-License-Identifier: Unlicensed
2+
pragma solidity ^0.8.16;
3+
4+
// Based on: https://github.com/rob-Hitchens/UnorderedKeySet/blob/master/contracts/HitchensUnorderedAddressSet.sol
5+
6+
library AddressSet {
7+
struct Set {
8+
mapping(address => uint256) keyPointers;
9+
address[] keyList;
10+
}
11+
12+
function insert(Set storage self, address key) internal {
13+
require(key != address(0), 'UnorderedKeySet(100) - Key cannot be 0x0');
14+
require(
15+
!exists(self, key),
16+
'UnorderedAddressSet(101) - Address (key) already exists in the set.'
17+
);
18+
self.keyList.push(key);
19+
self.keyPointers[key] = self.keyList.length - 1;
20+
}
21+
22+
function upsert(Set storage self, address key) internal {
23+
if (!exists(self, key)) {
24+
insert(self, key);
25+
}
26+
}
27+
28+
function remove(Set storage self, address key) internal {
29+
require(
30+
exists(self, key),
31+
'UnorderedKeySet(102) - Address (key) does not exist in the set.'
32+
);
33+
address keyToMove = self.keyList[count(self) - 1];
34+
uint256 rowToReplace = self.keyPointers[key];
35+
self.keyPointers[keyToMove] = rowToReplace;
36+
self.keyList[rowToReplace] = keyToMove;
37+
delete self.keyPointers[key];
38+
self.keyList.pop();
39+
}
40+
41+
function count(Set storage self) internal view returns (uint256) {
42+
return (self.keyList.length);
43+
}
44+
45+
function exists(Set storage self, address key)
46+
internal
47+
view
48+
returns (bool)
49+
{
50+
if (self.keyList.length == 0) return false;
51+
return self.keyList[self.keyPointers[key]] == key;
52+
}
53+
54+
function keyAtIndex(Set storage self, uint256 index)
55+
internal
56+
view
57+
returns (address)
58+
{
59+
return self.keyList[index];
60+
}
61+
}

deploy/0_GoldenProtocol.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { HardhatRuntimeEnvironment } from 'hardhat/types';
2+
import { DeployFunction } from 'hardhat-deploy/types';
3+
4+
const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
5+
const { deployments, getNamedAccounts } = hre;
6+
7+
const { deployer } = await getNamedAccounts();
8+
9+
await deployments.deploy('GoldenProtocol', {
10+
from: deployer,
11+
// skipIfAlreadyDeployed: true,
12+
args: [3],
13+
log: true,
14+
});
15+
};
16+
17+
deploy.id = 'deploy_golden_protocol';
18+
deploy.tags = ['GoldenProtocol'];
19+
20+
export default deploy;

test/GoldenProtocol.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { expect } from 'chai';
2+
import {
3+
deployments,
4+
ethers,
5+
getNamedAccounts,
6+
getUnnamedAccounts,
7+
} from 'hardhat';
8+
9+
import { setupUsers, setupUser, User, Contracts as _Contracts } from './utils';
10+
import getRandomBytesHexString from './utils/getRandomBytesHexString';
11+
import {
12+
GoldenProtocol as GoldenProtocolContract,
13+
GoldenProtocolQuestion as GoldenProtocolQuestionContract,
14+
} from '../typechain/contracts/GoldenProtocol.sol';
15+
16+
type Contracts = Pick<_Contracts, 'GoldenProtocol'>;
17+
18+
describe('GoldenProtocol', function () {
19+
let GoldenProtocol: Contracts['GoldenProtocol'];
20+
let owner: User<Contracts>;
21+
let users: User<Contracts>[];
22+
23+
beforeEach(async function () {
24+
await deployments.fixture(['GoldenProtocol']);
25+
GoldenProtocol = await ethers.getContract<GoldenProtocolContract>(
26+
'GoldenProtocol'
27+
);
28+
const contracts = { GoldenProtocol };
29+
const { deployer } = await getNamedAccounts();
30+
owner = await setupUser(deployer, contracts);
31+
users = await setupUsers(await getUnnamedAccounts(), contracts);
32+
});
33+
34+
describe('Deployment', function () {
35+
it('Should have correct owner', async function () {
36+
expect(await GoldenProtocol.owner()).to.equal(owner.address);
37+
});
38+
});
39+
40+
describe('Protocol', function () {
41+
it('asker can ask a question, answerer can answer, verifier can vote', async function () {
42+
const subjectUUID = getRandomBytesHexString(16);
43+
const predicateUUID = getRandomBytesHexString(16);
44+
const bounty = 10;
45+
const answer = 'Profound truth';
46+
47+
const asker = users[0];
48+
const answerer = users[1];
49+
const verifier = users[2];
50+
51+
// Ask question
52+
const transactionQuestion = await asker.GoldenProtocol.createQuestion(
53+
subjectUUID,
54+
predicateUUID,
55+
bounty
56+
);
57+
const resultQuestion = await transactionQuestion.wait();
58+
expect(resultQuestion.status).to.equal(1);
59+
60+
// Answer it
61+
const questionAddresses = await answerer.GoldenProtocol.questions();
62+
console.log({ questionAddresses });
63+
const GoldenProtocolQuestionAnswerer =
64+
await ethers.getContractAt<GoldenProtocolQuestionContract>(
65+
'GoldenProtocolQuestion',
66+
questionAddresses[0],
67+
answerer.address
68+
);
69+
expect(await GoldenProtocolQuestionAnswerer.owner()).to.equal(
70+
GoldenProtocol.address
71+
);
72+
expect(await GoldenProtocolQuestionAnswerer.asker()).to.equal(
73+
asker.address
74+
);
75+
expect(await GoldenProtocolQuestionAnswerer.subjectUUID()).to.equal(
76+
subjectUUID
77+
);
78+
expect(await GoldenProtocolQuestionAnswerer.predicateUUID()).to.equal(
79+
predicateUUID
80+
);
81+
expect(await GoldenProtocolQuestionAnswerer.bounty()).to.equal(bounty);
82+
const transactionAnswer = await GoldenProtocolQuestionAnswerer.addAnswer(
83+
answer
84+
);
85+
const resultAnswer = await transactionAnswer.wait();
86+
expect(resultAnswer.status).to.equal(1);
87+
88+
// Verify it
89+
const GoldenProtocolQuestionVerifier =
90+
await ethers.getContractAt<GoldenProtocolQuestionContract>(
91+
'GoldenProtocolQuestion',
92+
questionAddresses[0],
93+
verifier.address
94+
);
95+
expect(await GoldenProtocolQuestionVerifier.answers()).to.deep.equal([
96+
answer,
97+
]);
98+
const transactionVote = await GoldenProtocolQuestionVerifier.upvote(0);
99+
const resultVote = await transactionVote.wait();
100+
expect(resultVote.status).to.equal(1);
101+
});
102+
});
103+
});

test/utils/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { Address } from 'hardhat-deploy/types';
55
import type { GoldenSchemaGovernor } from '../../typechain/contracts/GoldenSchemaGovernor';
66
import type { GoldenSchema } from '../../typechain/contracts/GoldenSchema';
77
import type { GoldenToken } from '../../typechain/contracts/GoldenToken';
8+
import type { GoldenProtocol } from '../../typechain/contracts/GoldenProtocol.sol';
89

910
export type Contracts = {
1011
GoldenSchemaGovernor: GoldenSchemaGovernor;
1112
GoldenSchema: GoldenSchema;
1213
GoldenToken: GoldenToken;
14+
GoldenProtocol: GoldenProtocol;
1315
};
1416

1517
export type User<T> = { address: Address } & T;

0 commit comments

Comments
 (0)