-
Notifications
You must be signed in to change notification settings - Fork 0
Bounty smart contract #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
czechdave
wants to merge
9
commits into
master
Choose a base branch
from
feature/sc-18776/question-answer-smart-contract
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 5 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
f370af4
Rough first sketch of question/answer flow
czechdave 30a000d
Fix USD price reports
czechdave eb09ced
Fix slither errors
czechdave b747c34
Fix null checks, answers query
czechdave fa58768
Hook up ERC20 token and payout fnction
czechdave 67395ea
Refactor payout flows to support all actors
czechdave 8b3fe9b
Merge branch 'master' into feature/sc-18776/question-answer-smart-con…
czechdave 321e2d9
Fix merge
czechdave 1fcd62a
Next steps TODOs
czechdave File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
| // 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))); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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]; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.