Skip to content

Instantly share code, notes, and snippets.

@Jesserc
Created January 20, 2023 16:38
Show Gist options
  • Save Jesserc/bb98fc6016fdc4320510ca0e7871d0a8 to your computer and use it in GitHub Desktop.
Save Jesserc/bb98fc6016fdc4320510ca0e7871d0a8 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./proxy/Proxiable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract SurveysV2 {
using SafeMath for uint256;
using Counters for Counters.Counter;
Counters.Counter public _surveyPlanIDs; // holds unique IDs of each surveyPlan (Free, Basic or Corporate)
Counters.Counter public _surveyIDs; // variable that hold unique ID of each survey
Counters.Counter public _answerIDs;
Counters.Counter private _verificationIDs;
IERC20 public sonergyToken;
mapping(address => bool) isValidator; // this mapping tracks validators and returns true if an address is a validator
mapping(address => mapping(uint256 => bool)) private surveyParticipants;
mapping(address => mapping(uint256 => mapping(uint256 => bool))) validatorParticipants;
mapping(address => bool) isVerifiedUser; // this tracks verified users
mapping(address => bool) requestVerification;
mapping(uint256 => uint256) public surveyBalance; // track balance of a survey using survey ID
mapping(address => uint256) public surveyValidatorsBalance; // tracks the balance of each survey validator given the validators address
mapping(uint256 => uint256) public amountEachSurveyValidatorEarns;
mapping(uint256 => uint256) public amountEachSurveyParticipantEarns;
mapping(address => mapping(uint256 => uint256)) public validatorsProfit;
mapping(uint256 => SurveyPlans) listOfPlans; // holds all the survey plans and their IDs
mapping(uint256 => uint256) numberOfAnswers;
mapping(uint256 => uint256) numberOfValidators;
mapping(uint256 => SurveyItem) listOfSurveys; // tracks each surveyItem using the survey ID
mapping(uint256 => AnswerItem) listOfAnswers; // hold records of answers for each Survey using surveyID as key
uint256 public validatorsFee; // fee validators earn for validating surveys
uint256 public validatorsUnspendable;
uint256 public kycVerificationFee;
address private commissionAddress; // address that will receive the platform's commission
address owner;
/// @dev Struct that groups and hold layout of Survey Plans
struct SurveyPlans {
uint256 planID;
string planName;
uint256 minAmount;
uint256 validatorsProfit;
uint256 providerProfit;
bool status;
}
/// @dev This event is emitted when a plan is created
event planCreated(
uint256 planID,
string planName,
uint256 minAmount,
uint256 validatorsProfit,
uint256 providerProfit,
bool status
);
event kycStatus(address _user, bool isVerified);
/// @dev This struct holds a layout of Survey Items
struct SurveyItem {
string surveyURI;
address payable owner;
uint256 surveyID;
uint256 planID;
uint256 numOfValidators;
uint256 numOfcommisioners;
uint256 numOfResponse;
uint256 amount;
bool nftStatus;
bool exist;
bool completed;
}
/// @dev this event is emitted when a SurveyItem is created
event SurveyItemCreated(
string surveyURI,
address owner,
uint256 surveyID,
uint256 planID,
uint256 numOfValidators,
uint256 numOfcommisioners,
uint256 numOfResponse,
uint256 amount,
bool nftStatus,
bool exist,
bool completed
);
/// @dev This struct holds a layout of Answer Items of answers provided for each surveys
struct AnswerItem {
string answerURI;
address payable provider;
address payable validator;
uint256 surveyID;
uint256 answerID;
bool isValidated;
bool isValid;
}
/// @dev this event is emitted when an answerItem is created or an answer is provided to a surveyItem
event AnswerCreated(
string surveyURI,
address provider,
address validator,
uint256 surveyID,
uint256 answerID,
bool isValidated,
bool isValid
);
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "access for only owner");
_;
}
/// @dev this function sets up some state variables with actual values (it has an onlyOwner modifier)
/// it sets the sonergyToken contract address, address to receive platform's commission
function initialSetups(
address _tokenAddress,
address _commissionAddress,
address _userVerification
) external onlyOwner {
sonergyToken = IERC20(_tokenAddress);
commissionAddress = _commissionAddress;
//TODO: verify user on initial setup
requestVerification[_userVerification] = true;
isVerifiedUser[_userVerification] = true;
}
/// @dev this function updates the validator's fee (that is fee one pays to become a validator)
function updateValidatorsFee(
uint256 _validatorsFee,
uint256 _validatorsUnspendable
) external onlyOwner {
validatorsFee = _validatorsFee;
validatorsUnspendable = _validatorsUnspendable;
}
/// @dev this function is called by anyone who wants to become a validator
/// and the intended validator have approved this contract to spend a certain amount of token
/// this function ensures that the caller has enough sonergy token which must be greater than or equal
/// the validators fee (amount required to become a validator)
function becomeValidator() external {
require(
sonergyToken.balanceOf(msg.sender) >= validatorsFee,
"Insufficient Sonergy Tokens Available"
);
require(
sonergyToken.allowance(msg.sender, address(this)) >= validatorsFee,
"Insufficient Sonergy Tokens Allowance"
);
uint256 totalAmount = validatorsFee + validatorsUnspendable;
require(
sonergyToken.transferFrom(msg.sender, address(this), totalAmount),
"Failed to transfer Funds "
);
surveyValidatorsBalance[msg.sender] = validatorsUnspendable;
isValidator[msg.sender] = true;
}
/// @dev this function is used to remove a validator from being a validator
function undoValidator() external returns (bool) {
require(
surveyValidatorsBalance[msg.sender] > 0,
"You do not have an unspendable amount as a validator"
);
require(isValidator[msg.sender], "You are not a validator on Sonergy");
isValidator[msg.sender] = false;
uint256 amountToSend = surveyValidatorsBalance[msg.sender];
surveyValidatorsBalance[msg.sender] = 0;
bool success = sonergyToken.transfer(msg.sender, amountToSend);
require(success, "Transfer failed");
return success;
}
function fecthValidatorsUnspendable(
address user
) external view returns (uint256) {
return surveyValidatorsBalance[user];
}
function updateKYCFee(uint256 _kycVerificationFee) external onlyOwner {
kycVerificationFee = _kycVerificationFee;
}
/// @dev this function adds a new survey plan
function addSurveyPlans(
string memory _planName,
uint256 _minAmount,
uint256 _validatorsPercentProfit,
uint256 _providerProfit,
bool _display
) external onlyOwner {
_surveyPlanIDs.increment();
uint256 newPlanID = _surveyPlanIDs.current();
listOfPlans[newPlanID] = SurveyPlans(
newPlanID,
_planName,
_minAmount,
_validatorsPercentProfit,
_providerProfit,
_display
);
emit planCreated(
newPlanID,
_planName,
_minAmount,
_validatorsPercentProfit,
_providerProfit,
_display
);
}
/// @dev this returns the value of the state variables validatorsFee and the validatorsUnspendable
function fetchValidatorsFee() external view returns (uint256, uint256) {
return (validatorsFee, validatorsUnspendable);
}
/// @dev this function returns and array of survey plans whose status is true
/// the function gets the plans from the listOfPlans mapping and puts it into an array
function fetchSurveyPlans() external view returns (SurveyPlans[] memory) {
uint256 totalPlansCount = _surveyPlanIDs.current();
uint256 itemIndex = 0;
SurveyPlans[] memory items = new SurveyPlans[](totalPlansCount);
// Looping through the Plans and returning the active ones
for (uint256 i = 0; i < totalPlansCount; i++) {
if (listOfPlans[i + 1].status == true) {
uint256 currentID = i + 1;
SurveyPlans storage currentPlan = listOfPlans[currentID];
items[itemIndex] = currentPlan;
itemIndex += 1;
}
}
return items;
}
/// @dev this function edits the value of an existing survey plan using the survey plan ID
function editSurveyPlan(
uint256 _planID,
string memory _planName,
uint256 _minAmount,
uint256 _validatorsPercentProfit,
uint256 _providerProfit,
bool _display
) external onlyOwner {
uint256 priceOfPlan = _minAmount;
listOfPlans[_planID] = SurveyPlans(
_planID,
_planName,
priceOfPlan,
_validatorsPercentProfit,
_providerProfit,
_display
);
emit planCreated(
_planID,
_planName,
priceOfPlan,
_validatorsPercentProfit,
_providerProfit,
_display
);
}
/// @dev this function is used to verify a user, using the user's address
function verifyUser(address user) external onlyOwner {
require(
requestVerification[user],
"User needs to submit appeal for verification"
);
isVerifiedUser[user] = true;
bool isVerified = isVerifiedUser[user];
emit kycStatus(user, isVerified);
}
/// @dev this function enrolls a survey (only verified users can enroll a survey)
function createSurvey(
string memory surveyURI,
uint256 _planID,
uint256 _numOfValidators,
uint256 _numOfcommisioners,
uint256 _amount
) external {
require(
_planExist(_planID) != false,
"The Plan entered does not exist or its suspended. "
);
uint256 planAmount = listOfPlans[_planID].minAmount;
require(
_amount >= planAmount,
"Amount must be greater then the plan Amount"
);
require(
isVerifiedUser[msg.sender],
"You need to be verified to add a survey."
);
require(
sonergyToken.allowance(msg.sender, address(this)) >= _amount,
"Insufficient Sonergy Tokens Available"
);
// Initiate funds transfer.
require(
sonergyToken.transferFrom(msg.sender, address(this), _amount),
"Failed to transfer Funds "
);
_surveyIDs.increment();
uint256 newSurveyID = _surveyIDs.current();
// Create a balance for the survey
listOfSurveys[newSurveyID] = SurveyItem(
surveyURI,
payable(msg.sender),
newSurveyID,
_planID,
_numOfValidators,
_numOfcommisioners,
0,
_amount,
false,
true,
false
);
bool success = splitFunds(
_amount,
_planID,
newSurveyID,
_numOfcommisioners,
_numOfValidators
);
require(success, "Sonergy: Failed to distribute funds");
emit SurveyItemCreated(
surveyURI,
payable(msg.sender),
newSurveyID,
_planID,
_numOfValidators,
_numOfcommisioners,
0,
_amount,
false,
true,
false
);
}
/// @dev request for a user to be verified (only contract owner can call this function)
function requestUserVerification(address user) external onlyOwner {
requestVerification[user] = true;
isVerifiedUser[user] = false;
}
function getUserVerificationRequest(
address user
) external view returns (bool) {
return requestVerification[user];
}
/// @dev this function returns the verification status of the msg.sender(the user calling this function)
function fetchVerificationStatus() external view returns (bool) {
return isVerifiedUser[msg.sender];
}
/// @dev this function returns the verification status of an address that is passed to it as an argument
function isUserVerified(address user) internal view returns (bool) {
return isVerifiedUser[user];
}
function getAmountToEarn(
uint256 _surveyId
) external view returns (uint256, uint256) {
return (
amountEachSurveyValidatorEarns[_surveyId],
amountEachSurveyParticipantEarns[_surveyId]
);
}
/// @dev this function returns all the surveys created by the msg.sender(caller of this function)
function fetchMYSurveys() external view returns (SurveyItem[] memory) {
uint256 totalSurveyCount = _surveyIDs.current();
uint256 itemCount;
uint256 currentIndex;
for (uint256 i = 0; i < totalSurveyCount; i++) {
if (listOfSurveys[i + 1].owner == msg.sender) {
itemCount += 1;
}
}
SurveyItem[] memory items = new SurveyItem[](itemCount);
for (uint256 i = 0; i < totalSurveyCount; i++) {
if (listOfSurveys[i + 1].owner == msg.sender) {
uint256 currentID = i + 1;
SurveyItem storage currentItem = listOfSurveys[currentID];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}
/// @dev this function returns an array of all available surveys (surveys that the status is not equal to completed)
function fetchAvailableSurveys()
external
view
returns (SurveyItem[] memory)
{
uint256 totalSurveyCount = _surveyIDs.current();
uint256 itemCount = 0;
uint256 currentIndex = 0;
for (uint256 i = 0; i < totalSurveyCount; i++) {
if (listOfSurveys[i + 1].completed == false) {
itemCount += 1;
}
}
SurveyItem[] memory items = new SurveyItem[](itemCount);
for (uint256 i = 0; i < totalSurveyCount; i++) {
if (listOfSurveys[i + 1].completed == false) {
uint256 currentID = i + 1;
SurveyItem storage currentItem = listOfSurveys[currentID];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}
function fetchNFTSurveys() external view returns (SurveyItem[] memory) {
uint256 totalSurveyCount = _surveyIDs.current();
uint256 itemCount = 0;
uint256 currentIndex = 0;
for (uint256 i = 0; i < totalSurveyCount; i++) {
if (listOfSurveys[i + 1].nftStatus == true) {
itemCount += 1;
}
}
SurveyItem[] memory items = new SurveyItem[](itemCount);
for (uint256 i = 0; i < totalSurveyCount; i++) {
if (listOfSurveys[i + 1].nftStatus == true) {
uint256 currentID = i + 1;
SurveyItem storage currentItem = listOfSurveys[currentID];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}
/// @dev this function returns and array of all surveys that have been completed
function fetchCompletedSurveys()
external
view
returns (SurveyItem[] memory)
{
uint256 totalSurveyCount = _surveyIDs.current();
uint256 itemCount = 0;
uint256 currentIndex = 0;
for (uint256 i = 0; i < totalSurveyCount; i++) {
if (listOfSurveys[i + 1].completed == true) {
itemCount += 1;
}
}
SurveyItem[] memory items = new SurveyItem[](itemCount);
for (uint256 i = 0; i < totalSurveyCount; i++) {
if (listOfSurveys[i + 1].completed == true) {
uint256 currentID = i + 1;
SurveyItem storage currentItem = listOfSurveys[currentID];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}
/// @dev this function enables a user to provide an answer for a survey
/// it checks to ensure that the survey ID for which answer is to be provided exist
/// it also checks if the number of answers required for a survey is reached and reverts if it is
/// ensures that a user has not provided answer to a survey before(a user can provide answer twice to the same survey)
function provideAnswer(
uint256 _surveyID,
string memory answerURI
) external {
//Check if survey exist
require(_surveyExist(_surveyID), "Survey ID does not exist on Sonergy");
// Check if number of answers are complete
require(
!isAnswersComplete(_surveyID),
"This survey is already completed"
);
require(
!hasParticipated(_surveyID, msg.sender),
"You cannot Provide an answer again to this survey"
);
surveyParticipants[msg.sender][_surveyID] = true;
listOfSurveys[_surveyID].numOfResponse += 1;
listOfSurveys[_surveyID].numOfcommisioners += 1;
//increase number of answer provided for the survey
numberOfAnswers[_surveyID] += 1;
_answerIDs.increment();
uint256 newAnswerID = _answerIDs.current();
listOfAnswers[newAnswerID] = AnswerItem(
answerURI,
payable(msg.sender),
payable(address(0)),
_surveyID,
newAnswerID,
false,
false
);
emit AnswerCreated(
answerURI,
payable(msg.sender),
payable(address(0)),
_surveyID,
newAnswerID,
false,
false
);
sendProviderFunds(msg.sender, _surveyID);
}
/// @dev this function returns an array of all the answers provided for a particular surveyItem using the surveyID
function fetchSurveyAnswers(
uint256 _surveyID
) external view returns (AnswerItem[] memory) {
uint256 totalAnswersCount = _answerIDs.current();
uint256 itemCount = 0;
uint256 currentIndex = 0;
uint256 theSurveyId = _surveyID;
for (uint256 i = 0; i < totalAnswersCount; i++) {
if (listOfAnswers[i + 1].surveyID == theSurveyId) {
itemCount += 1;
}
}
AnswerItem[] memory items = new AnswerItem[](itemCount);
for (uint256 i = 0; i < totalAnswersCount; i++) {
if (listOfAnswers[i + 1].surveyID == _surveyID) {
uint256 currentID = i + 1;
AnswerItem storage currentItem = listOfAnswers[currentID];
items[currentIndex] = currentItem;
currentIndex += 1;
}
}
return items;
}
/// @dev this function checks if a user has participated or provided an answer to a particular survey
/// whose ID is passed as argument
function hasParticipated(
uint256 _surveyID,
address _user
) internal view returns (bool) {
if (surveyParticipants[_user][_surveyID]) {
return true;
}
return false;
}
/// @dev this function checks if an answer has been validated.
/// it takes the surveyID, answerID and userID(validator)
function hasValidatedAnswer(
uint256 _surveyID,
uint256 _answerID,
address _user
) internal view returns (bool) {
if (validatorParticipants[_user][_surveyID][_answerID]) {
return true;
}
return false;
}
/// @dev this function is used to validate an answer
/// it takes the surveyID, answerID and isValid (which is the response whether an answer is valid or not : true or false)
function validateAnswer(
uint256 _answerID,
uint256 _surveyID,
bool _isValid
) external {
uint256 currentValidators = numberOfValidators[_surveyID];
uint256 requiredValidators = listOfSurveys[_surveyID].numOfValidators;
require(
currentValidators <= requiredValidators,
"Required number of validators reached"
);
require(
!validatorParticipants[msg.sender][_surveyID][_answerID],
"You cannot validate this answer again"
);
require(
sonergyToken.allowance(msg.sender, address(this)) >= validatorsFee,
"You don't have the required number of SONERGY tokens to be a validator"
);
// require statement that ensures that a validator pays a validator's fee before validating
require(
sonergyToken.transferFrom(msg.sender, address(this), validatorsFee),
"Failed to transfer funds"
);
numberOfValidators[_surveyID] += 1;
listOfSurveys[_surveyID].numOfValidators += 1;
listOfAnswers[_answerID].isValidated = true;
listOfAnswers[_answerID].isValid = _isValid;
if (
(currentValidators + 1) == requiredValidators ||
requiredValidators == 1
) {
listOfSurveys[_surveyID].completed = true;
}
surveyParticipants[msg.sender][_surveyID] = true;
validatorParticipants[msg.sender][_surveyID][_answerID] = true;
sendValidatorsFunds(msg.sender, _surveyID);
}
function sendValidatorsFunds(
address _user,
uint256 surveyID
) internal returns (bool success) {
uint256 plan = listOfSurveys[surveyID].planID;
uint256 profit = listOfPlans[plan].validatorsProfit;
if (profit != 0) {
uint256 numOfResponse = listOfSurveys[surveyID].numOfValidators;
uint256 balanceInSurvey = surveyBalance[surveyID];
if (balanceInSurvey > 0 && numOfResponse != 0) {
uint256 validatorAmt = amountEachSurveyValidatorEarns[surveyID];
surveyBalance[surveyID] =
surveyBalance[surveyID] -
validatorAmt;
success = sonergyToken.transfer(_user, validatorAmt);
}
}
}
function sendProviderFunds(
address _user,
uint256 surveyID
) internal returns (bool success) {
uint256 plan = listOfSurveys[surveyID].planID;
uint256 profit = listOfPlans[plan].providerProfit;
if (profit != 0) {
uint256 numOfResponse = listOfSurveys[surveyID].numOfResponse;
uint256 numberofCommsioners = listOfSurveys[surveyID]
.numOfcommisioners;
uint256 balanceInSurvey = surveyBalance[surveyID];
if (balanceInSurvey > 0 && numOfResponse != numberofCommsioners) {
uint256 commisionerAmt = amountEachSurveyParticipantEarns[
surveyID
];
surveyBalance[surveyID] =
surveyBalance[surveyID] -
commisionerAmt;
success = sonergyToken.transfer(_user, commisionerAmt);
require(success, "Transfer failed");
}
}
}
function hasValidated(
uint256 _surveyID,
address _user
) internal view returns (bool) {
if (surveyParticipants[_user][_surveyID]) {
return true;
}
return false;
}
/// @dev this function sets the nftStatus of a survey to true
/// this is a flag that specifies if an NFT can be minted from a particular survey or not
/// if true(means NFT can be minted from the survey) if false (means NFT can't be minted from the survey)
/// This is used on the frontend
function makeNFT(uint256 _surveyID) external returns (uint256) {
require(_surveyExist(_surveyID), "Survey does not exist");
require(
listOfSurveys[_surveyID].owner == msg.sender,
"You are not the owner of the survey"
);
require(isAnswersComplete(_surveyID), "Survey is not completed yet.");
listOfSurveys[_surveyID].nftStatus = true;
return _surveyID;
}
/// @dev this function checks if answers can still be provided for a survey
/// it returns true if the numberOfAnswers provided equals required answers (meaning answers can't be provided again)
/// it returns false if numberOfAnswers provided is not equal required answers (meaning answers can be provided)
function isAnswersComplete(uint256 surveyID) internal view returns (bool) {
uint256 currentAnswers = numberOfAnswers[surveyID];
uint256 requiredAnswers = listOfSurveys[surveyID].numOfcommisioners;
if (currentAnswers == requiredAnswers) {
return true;
} else {
return false;
}
}
/// @dev this is an internal function used to derive both providers and validators earnings and transfers there
///the sonergy token left in the contract to the commissioner address
function splitFunds(
uint256 _amount,
uint256 _planID,
uint256 newSurveyID,
uint256 _numberofCommsioners,
uint256 _numberofValidators
) internal returns (bool success) {
uint256 valProfit = listOfPlans[_planID].validatorsProfit;
uint256 providersProfit = listOfPlans[_planID].providerProfit;
if (valProfit > 0) {
if (providersProfit > 0) {
uint256 validatorsAmt = (_amount * valProfit) / 100;
uint256 providersAmt = (_amount * providersProfit) / 100;
uint256 balance = _amount - (validatorsAmt + providersAmt);
surveyBalance[newSurveyID] += (validatorsAmt + providersAmt);
amountEachSurveyValidatorEarns[newSurveyID] += (validatorsAmt /
_numberofValidators);
amountEachSurveyParticipantEarns[newSurveyID] += (providersAmt /
_numberofCommsioners);
success = sonergyToken.transfer(commissionAddress, balance);
require(success, "Transfer failed");
}
}
}
/// @dev this checks if a survey exist (using the surveyID as argument)
function _surveyExist(uint256 surveyID) internal view returns (bool) {
if (listOfSurveys[surveyID].exist) {
return true;
}
return false;
}
/// @dev this function checks if a particular plan exist (using the planID as argument)
function _planExist(uint256 planID) internal view returns (bool) {
if (listOfPlans[planID].status) {
return true;
}
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment