Skip to content

Commit a368ef7

Browse files
committed
feat: Smart contract draft (not tested)
1 parent 4d1103c commit a368ef7

7 files changed

Lines changed: 221 additions & 36 deletions

File tree

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,14 @@ docs/
3737

3838
# Dotenv file
3939
.env
40+
41+
# Circuit files
42+
circuits/build
43+
*.r1cs
44+
*.sym
45+
*.wasm
46+
*.zkey
47+
*.ptau
48+
*.zkey
49+
verification_key.json
50+
Verifier.sol

circuits/TaskProof.circom

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
pragma circom 2.0.0;
2+
include "../node_modules/circomlib/circuits/poseidon.circom";
3+
4+
/*
5+
Public inputs (in this exact order as declared in main.public):
6+
0: commit -- Poseidon(solution, taskId, taskSalt)
7+
1: taskId
8+
2: taskSalt
9+
3: recipient -- uint256 (uint160(msg.sender)) as field element
10+
11+
Private inputs:
12+
solution
13+
14+
NOTE: component main {public [...]} ensures these inputs are exported as
15+
public inputs (and in that order) for snarkjs / verifier.sol.
16+
*/
17+
18+
template TaskProof() {
19+
// public inputs (declared here, but must be listed as public in main)
20+
signal input commit;
21+
signal input taskId;
22+
signal input taskSalt;
23+
signal input recipient;
24+
25+
// private input
26+
signal input solution;
27+
28+
// (1) commitment check: Poseidon([solution, taskId, taskSalt]) === commit
29+
component h = Poseidon(3);
30+
h.inputs[0] <== solution;
31+
h.inputs[1] <== taskId;
32+
h.inputs[2] <== taskSalt;
33+
h.out === commit;
34+
35+
// Note: recipient is a public input. By including it among the public inputs
36+
// of the proof you make the proof specific to that recipient value.
37+
// The contract must check recipient == msg.sender to enforce caller binding.
38+
}
39+
40+
component main { public [commit, taskId, taskSalt, recipient] } = TaskProof();

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
},
3333
"homepage": "https://github.com/imiskii/BountiBoard#readme",
3434
"dependencies": {
35+
"circomlib": "^2.0.5",
3536
"dotenv": "^17.2.1",
3637
"multiformats": "^13.3.7"
3738
}

src/TaskCreatorAccess.sol

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// SPDX-License-Identifier: GNU
2+
pragma solidity ^0.8.24;
3+
4+
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
5+
6+
contract TaskCreatorAccess {
7+
using EnumerableSet for EnumerableSet.AddressSet;
8+
9+
EnumerableSet.AddressSet private _taskCreators;
10+
11+
mapping(address => uint256) public voteCount;
12+
mapping(address => mapping(address => bool)) public hasVoted;
13+
14+
event TaskCreatorAdded(address indexed newCreator);
15+
event Voted(address indexed voter, address indexed candidate, uint256 votes);
16+
17+
modifier onlyTaskCreator() {
18+
require(isTaskCreator(msg.sender), "Not a TaskCreator");
19+
_;
20+
}
21+
22+
constructor(address initialCreator) {
23+
_taskCreators.add(initialCreator);
24+
}
25+
26+
function isTaskCreator(address account) public view returns (bool) {
27+
return _taskCreators.contains(account);
28+
}
29+
30+
function taskCreatorCount() public view returns (uint256) {
31+
return _taskCreators.length();
32+
}
33+
34+
function threshold() public view returns (uint256) {
35+
uint256 n = taskCreatorCount();
36+
if (n <= 20) {
37+
return (n / 2) + 1; // majority
38+
} else if (n < 200) {
39+
return n / 3; // ~1/3 when moderately sized
40+
} else {
41+
return 100; // capped threshold
42+
}
43+
}
44+
45+
function voteForTaskCreator(address candidate) external onlyTaskCreator {
46+
require(!isTaskCreator(candidate), "Already TaskCreator");
47+
require(!hasVoted[msg.sender][candidate], "Already voted");
48+
49+
hasVoted[msg.sender][candidate] = true;
50+
voteCount[candidate]++;
51+
52+
emit Voted(msg.sender, candidate, voteCount[candidate]);
53+
54+
if (voteCount[candidate] >= threshold()) {
55+
_taskCreators.add(candidate);
56+
emit TaskCreatorAdded(candidate);
57+
}
58+
}
59+
}

src/TaskManager.sol

Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,69 @@
1-
// SPDX-License-Identifier: MIT
1+
// SPDX-License-Identifier: GNU
22
pragma solidity ^0.8.24;
33

4-
import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
5-
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
4+
import "./TaskToken.sol";
5+
import "./TaskCreatorAccess.sol";
6+
import "./Verifier.sol";
67

7-
contract CTFProof is ERC1155, Ownable {
8-
// Mapping: taskId => metadata URI
9-
mapping(uint256 => string) private _taskURIs;
8+
contract TaskManager {
9+
CTFProof public ctfProof;
10+
PlonkVerifier public verifier;
11+
TaskCreatorAccess public accessControl;
1012

11-
// Optional: track existing task IDs
12-
uint256 public nextTaskId = 1;
13+
struct Task {
14+
uint256 commit; // Poseidon(solution, taskId, salt)
15+
uint256 salt; // Public salt
16+
bool exists;
17+
}
1318

14-
constructor(address initialOwner)
15-
ERC1155("") // base URI not used; we override uri()
16-
Ownable(initialOwner)
17-
{}
19+
mapping(uint256 => Task) public tasks;
20+
uint256 public nextTaskId;
1821

19-
/// @notice Add a new CTF task with its metadata URI
20-
function addTask(string memory taskURI) external onlyOwner returns (uint256 taskId) {
21-
taskId = nextTaskId++;
22-
_taskURIs[taskId] = taskURI;
23-
}
22+
event TaskCreated(uint256 indexed taskId, uint256 commit, uint256 salt);
23+
event TaskSolved(uint256 indexed taskId, address indexed solver);
2424

25-
/// @notice Mint proof token after validating solution (you call this after verification)
26-
function mintProof(address to, uint256 taskId) external onlyOwner {
27-
require(bytes(_taskURIs[taskId]).length > 0, "Invalid taskId");
28-
require(balanceOf(to, taskId) == 0, "Already claimed");
29-
_mint(to, taskId, 1, "");
25+
constructor(
26+
address _ctfProof,
27+
address _verifier,
28+
address _accessControl
29+
) {
30+
ctfProof = CTFProof(_ctfProof);
31+
verifier = PlonkVerifier(_verifier);
32+
accessControl = TaskCreatorAccess(_accessControl);
3033
}
3134

32-
/// @dev Prevent transfers except mint (from == 0) or burn (to == 0)
33-
function _update(
34-
address from,
35-
address to,
36-
uint256[] memory ids,
37-
uint256[] memory amounts
38-
) internal override {
39-
if (from != address(0) && to != address(0)) {
40-
revert("Transfers disabled");
41-
}
42-
super._update(from, to, ids, amounts);
35+
function addTask(uint256 commit, uint256 salt) external {
36+
require(accessControl.isTaskCreator(msg.sender), "Not TaskCreator");
37+
38+
uint256 taskId = ++nextTaskId;
39+
tasks[taskId] = Task(commit, salt, true);
40+
41+
emit TaskCreated(taskId, commit, salt);
4342
}
4443

45-
/// @notice Return token-specific URI
46-
function uri(uint256 taskId) public view override returns (string memory) {
47-
return _taskURIs[taskId];
44+
function solveTask(
45+
uint256 taskId,
46+
address recipient,
47+
uint256[24] calldata proof,
48+
uint256[4] calldata publicSignals
49+
) external {
50+
Task storage task = tasks[taskId];
51+
require(task.exists, "Task not found");
52+
53+
// Verify zkSNARK proof
54+
require(verifier.verifyProof(proof, publicSignals), "Invalid proof");
55+
56+
// Public signals expected order:
57+
// [0] = commit, [1] = taskId, [2] = salt, [3] = recipient
58+
require(publicSignals[0] == task.commit, "Commit mismatch");
59+
require(publicSignals[1] == taskId, "TaskId mismatch");
60+
require(publicSignals[2] == task.salt, "Salt mismatch");
61+
require(publicSignals[3] == uint256(uint160(recipient)), "Recipient mismatch");
62+
require(recipient == msg.sender, "Recipient must be caller");
63+
64+
// Mint ERC-1155 token (1 per solver per task)
65+
ctfProof.mintProof(msg.sender, taskId);
66+
67+
emit TaskSolved(taskId, msg.sender);
4868
}
4969
}

src/TaskToken.sol

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// SPDX-License-Identifier: GNU
2+
pragma solidity ^0.8.24;
3+
4+
import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
5+
6+
contract CTFProof is ERC1155 {
7+
// Mapping: taskId => metadata URI
8+
mapping(uint256 => string) private _taskURIs;
9+
10+
// Optional: track existing task IDs
11+
uint256 public nextTaskId = 1;
12+
13+
constructor()
14+
ERC1155("")
15+
{}
16+
17+
/// @notice Add a new CTF task with its metadata URI
18+
function addTask(string memory taskURI) external returns (uint256 taskId) {
19+
taskId = nextTaskId++;
20+
_taskURIs[taskId] = taskURI;
21+
}
22+
23+
/// @notice Mint proof token after validating solution (you call this after verification)
24+
function mintProof(address to, uint256 taskId) external {
25+
require(bytes(_taskURIs[taskId]).length > 0, "Invalid taskId");
26+
require(balanceOf(to, taskId) == 0, "Already claimed");
27+
_mint(to, taskId, 1, "");
28+
}
29+
30+
/// @dev Prevent transfers except mint (from == 0) or burn (to == 0)
31+
function _update(
32+
address from,
33+
address to,
34+
uint256[] memory ids,
35+
uint256[] memory amounts
36+
) internal override {
37+
if (from != address(0) && to != address(0)) {
38+
revert("Transfers disabled");
39+
}
40+
super._update(from, to, ids, amounts);
41+
}
42+
43+
/// @notice Return token-specific URI
44+
function uri(uint256 taskId) public view override returns (string memory) {
45+
return _taskURIs[taskId];
46+
}
47+
}

0 commit comments

Comments
 (0)