ForumExecutor
ForumExecutor.sol is the Phase 3 execution contract. 357 LOC, Solidity 0.8.26. A fork of
GitlawbBounty.sol (Apache-2.0, github.com/Gitlawb/contracts) with three changes:
- Token-agnostic (not hard-coded to
$GITLAWB). - N-of-M quorum from AGAINST-bonders (not single-creator approval).
- Atomic
BondingEscrow.settleplug-in on_approve/_reject.
See Protocol · Execution for the lifecycle. This page is the contract-level reference.
State machine
Open → Claimed → Submitted → (Approved | Rejected)
│ │
│ └─→ Open (dispute path)
│
└─→ CancelledStorage
enum Status { Open, Claimed, Submitted, Approved, Rejected, Cancelled }
struct Bounty {
address creator;
IERC20 token;
uint256 amount;
bytes32 ideaId;
string gitlawbRepo;
string acceptanceSpec;
uint64 createdAt;
uint64 claimDeadline; // seconds from createdAt
uint64 reviewDeadline; // seconds from submittedAt
Status status;
address claimantAddress;
string claimantDid;
string prId;
uint64 claimedAt;
uint64 submittedAt;
uint256 votesApprove; // weighted by AGAINST stake
uint256 votesReject;
}
mapping(uint256 => Bounty) public bounties;
mapping(uint256 => mapping(address => bool)) public hasVoted;
uint256 public nextBountyId;
IBondingEscrow public bondingEscrow;
address public treasury;
uint16 public protocolFeeBps; // 0..1000, default 500 (5%)
uint64 public defaultClaimDeadline; // default 1 day
uint64 public defaultReviewDeadline; // default 3 daysAudit L-05: bounties being public auto-generates a getter that exposes every field
including claimantDid (a string), inflating ABI surface. Pre-mainnet fix: mark it internal
and rely on the explicit getStatus, getPrId, getClaimant views.
createBounty
function createBounty(
address token,
uint256 amount,
bytes32 ideaId,
string calldata gitlawbRepo,
string calldata acceptanceSpec,
uint64 claimDeadline,
uint64 reviewDeadline
) external nonReentrant returns (uint256 bountyId) {
if (amount == 0) revert ZeroAmount();
bountyId = nextBountyId++;
Bounty storage b = bounties[bountyId];
b.creator = msg.sender;
b.token = IERC20(token);
b.amount = amount;
b.ideaId = ideaId;
b.gitlawbRepo = gitlawbRepo;
b.acceptanceSpec = acceptanceSpec;
b.createdAt = uint64(block.timestamp);
b.claimDeadline = claimDeadline == 0 ? defaultClaimDeadline : claimDeadline;
b.reviewDeadline = reviewDeadline == 0 ? defaultReviewDeadline : reviewDeadline;
b.status = Status.Open;
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
emit BountyCreated(bountyId, msg.sender, token, amount, ideaId);
}Audit H-02: any ERC-20 is accepted. Fee-on-transfer / rebasing / blacklist tokens brick
accounting. Pre-mainnet fix: require IChamberRegistry(registry).getIdea(token).exists.
claimBounty
function claimBounty(uint256 bountyId, string calldata claimantDid) external {
Bounty storage b = bounties[bountyId];
if (b.status != Status.Open) revert NotOpen();
if (block.timestamp > b.createdAt + b.claimDeadline) revert ClaimDeadlinePassed();
b.status = Status.Claimed;
b.claimantDid = claimantDid;
b.claimantAddress = msg.sender;
b.claimedAt = uint64(block.timestamp);
emit BountyClaimed(bountyId, msg.sender, claimantDid);
}submitBounty
function submitBounty(uint256 bountyId, string calldata prId) external {
Bounty storage b = bounties[bountyId];
if (b.status != Status.Claimed) revert NotClaimed();
if (msg.sender != b.claimantAddress) revert NotClaimant();
b.status = Status.Submitted;
b.prId = prId;
b.submittedAt = uint64(block.timestamp);
emit BountySubmitted(bountyId, prId);
}vote
function vote(uint256 bountyId, bool approve) external {
Bounty storage b = bounties[bountyId];
if (b.status != Status.Submitted) revert NotSubmitted();
if (hasVoted[bountyId][msg.sender]) revert AlreadyVoted();
uint256 weight = bondingEscrow.getPosition(bountyId, msg.sender).againstStake;
if (weight == 0) revert NoAgainstStake();
hasVoted[bountyId][msg.sender] = true;
if (approve) b.votesApprove += weight;
else b.votesReject += weight;
emit Voted(bountyId, msg.sender, approve, weight);
}Audit H-01: hasVoted is per-(bountyId, voter) only. After disputeBounty, voters from
the previous round cannot vote again. Pre-mainnet fix: introduce
voteRound[bountyId] counter and namespace votes by (bountyId, voteRound, voter).
finalize
function finalize(uint256 bountyId) external nonReentrant {
Bounty storage b = bounties[bountyId];
if (b.status != Status.Submitted) revert NotSubmitted();
IBondingEscrow.BountyState memory bs = bondingEscrow.getBounty(bountyId);
uint256 totalAgainst = uint256(bs.totalAgainst);
bool deadlinePassed = block.timestamp > b.submittedAt + b.reviewDeadline;
bool majorityApprove = totalAgainst > 0 && b.votesApprove * 2 > totalAgainst;
bool majorityReject = totalAgainst > 0 && b.votesReject * 2 > totalAgainst;
if (!deadlinePassed && !majorityApprove && !majorityReject) revert VotesNotConclusive();
bool approve = totalAgainst == 0 || b.votesApprove >= b.votesReject;
if (approve) _approve(bountyId, b);
else _reject(bountyId, b);
}Audit H-03: 1 wei of AGAINST stake is sufficient to short-circuit pre-deadline. Pre-mainnet
fix: minimum quorum threshold, e.g. max(b.amount / 100, 1 ether).
_approve / _reject
function _approve(uint256 bountyId, Bounty storage b) internal {
b.status = Status.Approved;
uint256 fee = (b.amount * protocolFeeBps) / 10_000;
uint256 payout = b.amount - fee;
b.token.safeTransfer(b.claimantAddress, payout);
if (fee > 0) b.token.safeTransfer(treasury, fee);
bondingEscrow.settle(bountyId, true);
emit BountyApproved(bountyId, b.claimantAddress, payout, fee);
}Audit M-07: current order is transfer → settle. Pre-mainnet fix: settle → transfer
(CEI). Same applies to _reject.
disputeBounty
If submittedAt + reviewDeadline passes without finalize, anyone can dispute. Status returns to
Open, claimant fields and votes are zeroed.
function disputeBounty(uint256 bountyId) external {
Bounty storage b = bounties[bountyId];
if (b.status != Status.Submitted) revert NotSubmitted();
if (block.timestamp <= b.submittedAt + b.reviewDeadline) revert NotDisputable();
b.status = Status.Open;
b.claimantDid = "";
b.claimantAddress = address(0);
b.prId = "";
b.claimedAt = 0;
b.submittedAt = 0;
b.votesApprove = 0;
b.votesReject = 0;
emit BountyDisputed(bountyId);
}Access control summary
| Function | Caller |
|---|---|
createBounty | anyone (with allowance) |
claimBounty | anyone, during claim window |
submitBounty | claimant only |
vote | AGAINST-bonders only (weight > 0) |
finalize, disputeBounty | anyone, when conditions met |
cancelBounty | creator, only while Open and not yet claimed |
| Setters | owner |