Skip to content

Implement Curyo 2 question reward pools foundation#126

Draft
Noc2 wants to merge 408 commits intomainfrom
codex/curyo-2-bounties-research
Draft

Implement Curyo 2 question reward pools foundation#126
Noc2 wants to merge 408 commits intomainfrom
codex/curyo-2-bounties-research

Conversation

@Noc2
Copy link
Copy Markdown
Owner

@Noc2 Noc2 commented Apr 15, 2026

Summary

  • Adds Curyo 2 question-first submissions, Celo USDC Question Reward Pool escrow, delegated Voter ID claim support, and focused Foundry coverage.
  • Wires deployment scripts, ABIs, Ponder indexing/API routes, frontend submit/fund/claim flows, bot source categories, and MCP hosted submit writes to the new question path.
  • Updates public docs, READMEs, legal copy, and regenerates the whitepaper PDF for optional question-scoped reward pools paid in USDC on Celo.

Verification

  • forge test --match-path test/QuestionRewardPoolEscrow.t.sol
  • forge test --match-path test/DeployCuryoCompilation.t.sol
  • yarn workspace @curyo/ponder test tests/question-reward-pool-escrow-handlers.test.ts tests/route-validation.test.ts ponder.config.test.ts
  • ./node_modules/.bin/tsx --test hooks/claimableRewards.test.ts hooks/contentFeed/shared.test.ts hooks/useContentFeedMetadata.test.ts lib/docs/whitepaperContent.test.ts
  • yarn workspace @curyo/bot test
  • yarn workspace @curyo/mcp-server test src/tests/signer-service.test.ts
  • yarn workspace @curyo/mcp-server check-types
  • yarn workspace @curyo/nextjs check-types
  • yarn workspace @curyo/ponder test
  • yarn workspace @curyo/nextjs test
  • forge build
  • forge test --match-contract QuestionRewardPoolEscrowTest

Notes

  • Launch reward settlement is Celo USDC only; no USDT selector, generic token support, or stablecoin coherence bonus is included.
  • Question Reward Pools are question-scoped and paid as equal per-round USDC participation shares for eligible revealed Voter ID holders.
  • Users should see USD-oriented reward language, with asset disclosure as Paid in USDC on Celo.
  • Live frontend reward pool escrow addresses will come from redeployment/generated artifacts; the frontend also supports an env fallback while deployment metadata catches up.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
curyo-nextjs Error Error Apr 21, 2026 4:13pm

Request Review

Comment thread packages/foundry/contracts/ContentRegistry.sol Fixed
Comment thread packages/foundry/contracts/QuestionBountyEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionBountyEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/ContentRegistry.sol Fixed
Comment thread packages/foundry/contracts/QuestionBountyEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionBountyEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
@Noc2 Noc2 changed the title Implement Curyo 2 bountied questions foundation Implement Curyo 2 question reward pools foundation Apr 15, 2026
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol
Comment thread packages/foundry/contracts/HumanFaucet.sol
Comment thread packages/foundry/contracts/HumanFaucet.sol
Comment thread packages/foundry/contracts/HumanFaucet.sol
Comment thread packages/foundry/contracts/HumanFaucet.sol
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol
Comment thread packages/foundry/contracts/ContentRegistry.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol Fixed
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol
Comment thread packages/foundry/contracts/QuestionRewardPoolEscrow.sol
Comment on lines +1184 to +1186
function _voterIdForRound(uint256 contentId, uint256 roundId, address account) internal view returns (uint256) {
return _roundVoterIdNft(contentId, roundId).getTokenId(account);
}
Comment on lines +1184 to +1186
function _voterIdForRound(uint256 contentId, uint256 roundId, address account) internal view returns (uint256) {
return _roundVoterIdNft(contentId, roundId).getTokenId(account);
}
Comment on lines +748 to +756
returns (uint256 resolvedCategoryId)
{
require(metadata.categoryId != 0, "Category required");
require(categoryRegistry.isCategory(metadata.categoryId), "Category not registered");
return metadata.categoryId;
}

function _deriveQuestionMediaSubmissionKey(SubmissionMetadata memory metadata, uint256 resolvedCategoryId)
internal
Comment on lines +630 to +646
function _validatedContextSubmissionMetadata(
string memory contextUrl,
string[] memory imageUrls,
string memory videoUrl,
string memory title,
string memory description,
string memory tags,
uint256 categoryId
) internal view returns (SubmissionMetadata memory metadata) {
SUBMISSION_MEDIA_VALIDATOR.validateContextUrl(contextUrl);
SUBMISSION_MEDIA_VALIDATOR.validateOptionalMediaSet(imageUrls, videoUrl);
metadata = SubmissionMetadata({
url: contextUrl, title: title, description: description, tags: tags, categoryId: categoryId
});
_validateTextFields(metadata);
require(address(categoryRegistry) != address(0), "CategoryRegistry not set");
}
Comment on lines +630 to +646
function _validatedContextSubmissionMetadata(
string memory contextUrl,
string[] memory imageUrls,
string memory videoUrl,
string memory title,
string memory description,
string memory tags,
uint256 categoryId
) internal view returns (SubmissionMetadata memory metadata) {
SUBMISSION_MEDIA_VALIDATOR.validateContextUrl(contextUrl);
SUBMISSION_MEDIA_VALIDATOR.validateOptionalMediaSet(imageUrls, videoUrl);
metadata = SubmissionMetadata({
url: contextUrl, title: title, description: description, tags: tags, categoryId: categoryId
});
_validateTextFields(metadata);
require(address(categoryRegistry) != address(0), "CategoryRegistry not set");
}
Comment on lines +783 to +806
function _requireCompletedBundle(uint256 bundleId, uint256 voterId)
internal
view
returns (address frontend, bytes32 firstCommitKey)
{
BundleQuestion[] storage questions = bundleQuestions[bundleId];
require(questions.length > 0, "No questions");
for (uint256 i = 0; i < questions.length; i++) {
BundleQuestion storage question = questions[i];
require(question.terminal && question.settled, "Question not settled");
bytes32 commitKey = votingEngine.voterIdCommitKey(question.contentId, question.roundId, voterId);
require(commitKey != bytes32(0), "No commit");
require(
votingEngine.commitVoterId(question.contentId, question.roundId, commitKey) == voterId, "Wrong Voter ID"
);
(bool revealed, address questionFrontend) =
_revealedCommitFrontend(question.contentId, question.roundId, commitKey);
require(revealed, "Vote not revealed");
if (i == 0) {
frontend = questionFrontend;
firstCommitKey = commitKey;
}
}
}
Comment on lines +808 to +820
function _hasCompletedBundle(uint256 bundleId, uint256 voterId) internal view returns (bool) {
BundleQuestion[] storage questions = bundleQuestions[bundleId];
if (questions.length == 0) return false;
for (uint256 i = 0; i < questions.length; i++) {
BundleQuestion storage question = questions[i];
if (!question.terminal || !question.settled) return false;
bytes32 commitKey = votingEngine.voterIdCommitKey(question.contentId, question.roundId, voterId);
if (commitKey == bytes32(0)) return false;
(bool revealed,) = _revealedCommitFrontend(question.contentId, question.roundId, commitKey);
if (!revealed) return false;
}
return true;
}
Comment on lines +822 to +840
function _isBundleExcludedVoter(BundleReward storage bundle, uint256 bundleId, uint256 voterId)
internal
view
returns (bool)
{
BundleQuestion[] storage questions = bundleQuestions[bundleId];
for (uint256 i = 0; i < questions.length; i++) {
BundleQuestion storage question = questions[i];
uint256 funderVoterId = _funderVoterIdForBundleQuestion(bundle, question.contentId, question.roundId);
if (voterId == funderVoterId) return true;

address submitterIdentity = registry.getSubmitterIdentity(question.contentId);
if (submitterIdentity != address(0)) {
uint256 submitterVoterId = _voterIdForRound(question.contentId, question.roundId, submitterIdentity);
if (voterId == submitterVoterId) return true;
}
}
return false;
}
Comment on lines +822 to +840
function _isBundleExcludedVoter(BundleReward storage bundle, uint256 bundleId, uint256 voterId)
internal
view
returns (bool)
{
BundleQuestion[] storage questions = bundleQuestions[bundleId];
for (uint256 i = 0; i < questions.length; i++) {
BundleQuestion storage question = questions[i];
uint256 funderVoterId = _funderVoterIdForBundleQuestion(bundle, question.contentId, question.roundId);
if (voterId == funderVoterId) return true;

address submitterIdentity = registry.getSubmitterIdentity(question.contentId);
if (submitterIdentity != address(0)) {
uint256 submitterVoterId = _voterIdForRound(question.contentId, question.roundId, submitterIdentity);
if (voterId == submitterVoterId) return true;
}
}
return false;
}
Comment on lines +1024 to +1032
function _revealedCommitFrontend(uint256 contentId, uint256 roundId, bytes32 commitKey)
internal
view
returns (bool revealed, address frontend)
{
(address voter,,,,, address commitFrontend,, bool commitRevealed,,) =
votingEngine.commits(contentId, roundId, commitKey);
return (voter != address(0) && commitRevealed, commitFrontend);
}
Comment on lines +1047 to +1049
mstore(add(add(encoded, 32), offset), value)
}
}
Comment on lines +411 to +515
function submitQuestionBundleWithRewardAndRoundConfig(
BundleQuestionInput[] calldata questions,
SubmissionRewardTerms calldata rewardTerms,
RoundLib.RoundConfig calldata roundConfig
) external nonReentrant whenNotPaused returns (uint256 bundleId, uint256[] memory contentIds) {
require(questions.length > 0, "No questions");
require(questions.length <= MAX_QUESTION_BUNDLE_COUNT, "Too many questions");
require(questionRewardPoolEscrow != address(0), "Bounty escrow not set");

RoundLib.RoundConfig memory validatedRoundConfig = _validatedRoundConfig(roundConfig);
_validateSubmissionReward(rewardTerms);

SubmissionMetadata[] memory metadataList = new SubmissionMetadata[](questions.length);
bytes32[] memory submissionKeys = new bytes32[](questions.length);
bytes32[] memory mediaHashes = new bytes32[](questions.length);
uint256[] memory resolvedCategoryIds = new uint256[](questions.length);

for (uint256 i = 0; i < questions.length; i++) {
BundleQuestionInput calldata question = questions[i];
SubmissionMetadata memory metadata = _validatedContextSubmissionMetadata(
question.contextUrl,
question.imageUrls,
question.videoUrl,
question.title,
question.description,
question.tags,
question.categoryId
);
uint256 resolvedCategoryId = _resolveQuestionSubmissionCategory(metadata);
bytes32 submissionKey = _deriveQuestionMediaSubmissionKey(metadata, resolvedCategoryId);
require(!submissionKeyUsed[submissionKey], "Question already submitted");

metadataList[i] = metadata;
submissionKeys[i] = submissionKey;
mediaHashes[i] = _submissionMediaHash(question.imageUrls, question.videoUrl);
resolvedCategoryIds[i] = resolvedCategoryId;
}

bytes32 bundleHash = _computeQuestionBundleHash(metadataList, mediaHashes, resolvedCategoryIds, questions);
bytes32 revealCommitment =
_computeBundleRevealCommitment(bundleHash, msg.sender, rewardTerms, validatedRoundConfig);
PendingSubmission memory pending = pendingSubmissions[revealCommitment];
require(pending.submitter == msg.sender, "Reservation not found");
require(block.timestamp <= pending.expiresAt, "Reservation expired");
require(block.timestamp >= pending.reservedAt + RESERVED_SUBMISSION_MIN_AGE, "Reservation too new");
delete pendingSubmissions[revealCommitment];

bundleId = nextQuestionBundleId++;
contentIds = new uint256[](questions.length);
for (uint256 i = 0; i < questions.length; i++) {
submissionKeyUsed[submissionKeys[i]] = true;
bytes32 contentHash = keccak256(
abi.encode(
"curyo-question-context-v1",
metadataList[i].url,
questions[i].imageUrls,
questions[i].videoUrl,
metadataList[i].title,
metadataList[i].description,
metadataList[i].tags,
resolvedCategoryIds[i]
)
);
uint256 contentId = _storeSubmittedContent(
submissionKeys[i], pending, contentHash, resolvedCategoryIds[i], validatedRoundConfig, bundleId, i
);
contentIds[i] = contentId;
questionBundleContentIds[bundleId].push(contentId);

emit ContentSubmitted(
contentId,
msg.sender,
contentHash,
metadataList[i].url,
metadataList[i].title,
metadataList[i].description,
metadataList[i].tags,
resolvedCategoryIds[i]
);
emit ContentMediaSubmitted(contentId, questions[i].imageUrls, questions[i].videoUrl);
emit QuestionBundleContentLinked(bundleId, contentId, i);
}

uint256 rewardPoolId = IQuestionRewardPoolEscrow(questionRewardPoolEscrow)
.createSubmissionBundleFromRegistry(
bundleId,
contentIds,
msg.sender,
rewardTerms.asset,
rewardTerms.amount,
rewardTerms.requiredVoters,
rewardTerms.expiresAt
);
emit QuestionBundleSubmitted(
bundleId,
msg.sender,
questions.length,
rewardTerms.asset,
rewardTerms.amount,
rewardTerms.requiredVoters,
rewardTerms.expiresAt,
bundleHash,
rewardPoolId
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants