Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions packages/foundry/contracts/Bounty.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@ contract Bounty {
Public // plaintext CID (public)
}

// Severity levels for triage
enum Severity {
None,
Low,
Medium,
High,
Critical
}

address public owner;
address public triager; // Triager can view reports and assign severity
uint256 public amount;
string public cid; // IPFS CID for bounty details (title, description, severity)
Status public status;
Expand All @@ -40,6 +50,7 @@ contract Bounty {
uint256 stake;
SubmissionState state;
Visibility visibility; // 0=Private, 1=Public
Severity severity; // Assigned by triager
}

mapping(address => Submission) private _submissions; // per-wallet single submission
Expand All @@ -53,13 +64,15 @@ contract Bounty {
event StakeRefunded(address indexed researcher, uint256 amount);
event StakeSlashed(address indexed researcher, uint256 amount, address indexed receiver);
event BountyClosed(uint256 winners, uint256 totalPaid);
event SeveritySet(address indexed researcher, Severity severity);
event SubmissionVisibilityChanged(address indexed researcher, Visibility visibility, string cid);

constructor(address _owner, string memory _cid, uint256 _stakeAmount, uint256 _duration) payable {
constructor(address _owner, string memory _cid, uint256 _stakeAmount, uint256 _duration, address _triager) payable {
require(msg.value > 0, "Bounty amount cannot be zero");
require(_stakeAmount > 0, "Stake amount must be > 0");
require(_duration > 0, "Duration must be > 0");
owner = _owner;
triager = _triager;
cid = _cid;
amount = msg.value;
status = Status.Open;
Expand Down Expand Up @@ -199,16 +212,17 @@ contract Bounty {

/**
* @dev Update a submission's visibility and CID.
* Only bounty owner or the submission's researcher can call it.
* Only bounty owner, triager, or the submission's researcher can call it.
* Researcher cannot publish while Pending or while owner is fixing.
*/
function setSubmissionVisibility(address _researcher, Visibility _visibility, string calldata _newCid) external {
Submission storage subm = _submissions[_researcher];
require(subm.state != SubmissionState.None, "No submission");

bool isOwner = msg.sender == owner;
bool isTriager = msg.sender == triager;
bool isResearcher = msg.sender == _researcher;
require(isOwner || isResearcher, "Not allowed");
require(isOwner || isTriager || isResearcher, "Not allowed");

if (isResearcher) {
require(subm.state != SubmissionState.Pending, "Not allowed while Pending");
Expand All @@ -218,6 +232,19 @@ contract Bounty {
subm.visibility = _visibility;
emit SubmissionVisibilityChanged(_researcher, _visibility, _newCid);
}

/**
* @dev Set the severity of a submission. Only the triager can call this.
* @param _researcher The address of the researcher whose submission to update.
* @param _severity The severity level to assign.
*/
function setSeverity(address _researcher, Severity _severity) external {
require(msg.sender == triager, "Only triager can set severity");
Submission storage subm = _submissions[_researcher];
require(subm.state != SubmissionState.None, "No submission");
subm.severity = _severity;
emit SeveritySet(_researcher, _severity);
}
/**
* @dev Returns the list of submitter addresses.
*/
Expand All @@ -230,8 +257,8 @@ contract Bounty {
*/
function getSubmission(
address _researcher
) external view returns (string memory, uint256, SubmissionState, Visibility) {
) external view returns (string memory, uint256, SubmissionState, Visibility, Severity) {
Submission storage s = _submissions[_researcher];
return (s.reportCid, s.stake, s.state, s.visibility);
return (s.reportCid, s.stake, s.state, s.visibility, s.severity);
}
}
11 changes: 7 additions & 4 deletions packages/foundry/contracts/BountyFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ contract BountyFactory {
string cid,
uint256 amount,
uint256 stakeAmount,
uint256 duration
uint256 duration,
address triager
);

constructor() {}
Expand All @@ -25,21 +26,23 @@ contract BountyFactory {
* @dev Creates and deploys a new Bounty contract, funding it with the sent ETH.
* @param _owner The address that will own the new bounty.
* @param _cid The IPFS CID for the bounty's metadata.
* @param _triager The address that will triage submissions (can be zero address).
* @return The address of the newly created Bounty contract.
*/
function createBounty(
address _owner,
string memory _cid,
uint256 _stakeAmount,
uint256 _duration
uint256 _duration,
address _triager
) external payable returns (address) {
// Forward ETH to new bounty as reward pool
Bounty newBounty = new Bounty{ value: msg.value }(_owner, _cid, _stakeAmount, _duration);
Bounty newBounty = new Bounty{ value: msg.value }(_owner, _cid, _stakeAmount, _duration, _triager);

address newBountyAddress = address(newBounty);
deployedBounties.push(newBountyAddress);

emit BountyCreated(newBountyAddress, _owner, _cid, msg.value, _stakeAmount, _duration);
emit BountyCreated(newBountyAddress, _owner, _cid, msg.value, _stakeAmount, _duration, _triager);
return newBountyAddress;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/nextjs/app/blockexplorer/address/[address]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from "fs";
import type { Metadata } from "next";
import path from "path";
import { Address } from "viem";
import { hardhat } from "viem/chains";
import { anvil } from "viem/chains";
import { AddressComponent } from "~~/app/blockexplorer/_components/AddressComponent";
import deployedContracts from "~~/contracts/deployedContracts";
import { isZeroAddress } from "~~/utils/scaffold-eth/common";
Expand Down Expand Up @@ -46,7 +46,7 @@ async function fetchByteCodeAndAssembly(buildInfoDirectory: string, contractPath

const getContractData = async (address: Address) => {
const contracts = deployedContracts as GenericContractsDeclaration | null;
const chainId = hardhat.id;
const chainId = anvil.id;

if (!contracts || !contracts[chainId] || Object.keys(contracts[chainId]).length === 0) {
return null;
Expand All @@ -63,7 +63,7 @@ const getContractData = async (address: Address) => {
"..",
"..",
"..",
"hardhat",
"anvil",
"artifacts",
"build-info",
);
Expand Down
16 changes: 9 additions & 7 deletions packages/nextjs/app/bounties/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function BountyDetailsPage() {
const [description, setDescription] = useState("");
const [contact, setContact] = useState("");
const [stakeEth, setStakeEth] = useState("0.00");
const [metadata, setMetadata] = useState({ title: "Loading...", description: "Loading...", severity: "Medium" });
const [metadata, setMetadata] = useState({ title: "Loading...", description: "Loading..." });
const [submitting, setSubmitting] = useState(false);
const [submitters, setSubmitters] = useState<string[]>([]);
const [committedAmount, setCommittedAmount] = useState<bigint>(0n);
Expand All @@ -49,13 +49,14 @@ export default function BountyDetailsPage() {
{ address: bountyAddress, abi: bountyABI, functionName: "stakeAmount" },
{ address: bountyAddress, abi: bountyABI, functionName: "endTime" },
{ address: bountyAddress, abi: bountyABI, functionName: "getSubmitters" },
{ address: bountyAddress, abi: bountyABI, functionName: "triager" },
],
query: {
refetchInterval: 5000,
},
});

const [owner, amount, cid, status, stakeAmount, endTimeResult, submittersResult] = bountyData || [];
const [owner, amount, cid, status, stakeAmount, endTimeResult, submittersResult, triagerResult] = bountyData || [];

const { isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash });

Expand Down Expand Up @@ -113,7 +114,6 @@ export default function BountyDetailsPage() {
setMetadata({
title: meta.title || `Bounty: ${bountyAddress.substring(0, 8)}...`,
description: meta.description || "No description provided",
severity: meta.severity || "Medium",
});
} else {
throw new Error(`Lighthouse gateway returned status ${response.status}`);
Expand Down Expand Up @@ -311,10 +311,12 @@ export default function BountyDetailsPage() {
<h3 className="font-roboto text-sm mb-2 text-gray-500">Posted by</h3>
<Address address={owner?.result as string} />
</div>
<div>
<h3 className="font-roboto text-sm mb-2 text-gray-500">Severity</h3>
<p className="font-roboto text-white">{metadata.severity}</p>
</div>
{triagerResult?.result && triagerResult.result !== "0x0000000000000000000000000000000000000000" && (
<div>
<h3 className="font-roboto text-sm mb-2 text-gray-500">Triager</h3>
<Address address={triagerResult.result as string} />
</div>
)}
<div>
<h3 className="font-roboto text-sm mb-2 text-gray-500">Stake</h3>
<div className="flex items-center gap-2">
Expand Down
44 changes: 22 additions & 22 deletions packages/nextjs/app/bounties/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
CurrencyDollarIcon,
DocumentTextIcon,
PencilSquareIcon,
ShieldExclamationIcon,
ShieldCheckIcon,
UserCircleIcon,
} from "@heroicons/react/24/outline";
import MDEditor from "~~/components/MDEditor";
Expand All @@ -23,7 +23,7 @@ type BountyForm = {
description: string;
amount: string;
projectAddress: string;
severity: "Low" | "Medium" | "High" | "Critical";
triagerAddress: string;
stake: string;
durationDays: string;
};
Expand Down Expand Up @@ -55,7 +55,7 @@ export default function CreateBountyPage() {
description: "",
amount: "",
projectAddress: connectedAddress || "",
severity: "Medium",
triagerAddress: "",
stake: "0.01",
durationDays: "7",
});
Expand Down Expand Up @@ -92,7 +92,6 @@ export default function CreateBountyPage() {
JSON.stringify({
title: form.title,
description: form.description,
severity: form.severity,
}),
apiKey,
);
Expand All @@ -101,7 +100,13 @@ export default function CreateBountyPage() {

await writeBountyFactoryAsync({
functionName: "createBounty",
args: [form.projectAddress, cid, parseEther(form.stake), BigInt(durationDaysNum * 24 * 60 * 60)],
args: [
form.projectAddress,
cid,
parseEther(form.stake),
BigInt(durationDaysNum * 24 * 60 * 60),
form.triagerAddress || "0x0000000000000000000000000000000000000000",
],
value: parseEther(form.amount),
});

Expand Down Expand Up @@ -176,23 +181,6 @@ export default function CreateBountyPage() {
placeholder="7"
/>
</FormField>

<FormField
label="Severity"
icon={<ShieldExclamationIcon className="h-5 w-5" />}
helperText="How critical is the potential vulnerability?"
>
<select
value={form.severity}
onChange={e => setForm({ ...form, severity: e.target.value as BountyForm["severity"] })}
className="w-full px-4 py-3 bg-black border border-gray-800 text-white font-roboto focus:outline-none focus:border-[var(--color-secondary)]/50 transition-colors"
>
<option value="Low">Low</option>
<option value="Medium">Medium</option>
<option value="High">High</option>
<option value="Critical">Critical</option>
</select>
</FormField>
</div>
</div>

Expand All @@ -208,6 +196,18 @@ export default function CreateBountyPage() {
/>
</FormField>

<FormField
label="Triager Address (Optional)"
icon={<ShieldCheckIcon className="h-5 w-5" />}
helperText="This address can view reports and assign severity levels. Leave empty if not needed."
>
<AddressInput
value={form.triagerAddress}
onChange={value => setForm({ ...form, triagerAddress: value })}
placeholder="The address that will triage submissions"
/>
</FormField>

<div className="pt-6">
<button
type="submit"
Expand Down
Loading
Loading