Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Core Contracts

Two contracts handle all onchain coordination, deployed on Ethereum (Sepolia testnet).


ProjectRegistry

Stores the canonical onchain state for every project in the network. Radicle project IDs are append-only — once registered, the association between an onchain project and its Radicle repo(s) is permanent and auditable.

State:
  • mapping(uint256 => Project) — project data keyed by numeric ID
  • mapping(uint256 => mapping(address => bool)) maintainers — per-project maintainer roles
  • mapping(uint256 => mapping(address => bool)) allowlist — per-project bounty-creation allowlist
Project struct:
struct Project {
  address owner;
  string  name;
  string  description;
  string[] radicleIds;   // append-only; index 0 is the canonical Radicle ID
}
Functions:
ProjectRegistry
├── createProject(radicleId, name, description) → projectId
├── addRadicleId(projectId, radicleId)           // owner only; append-only
├── addMaintainer(projectId, address)            // owner only
├── removeMaintainer(projectId, address)         // owner only
├── updateAllowlist(projectId, address, bool)    // owner only
├── transferOwnership(projectId, newOwner)       // owner only
├── canCreateBounty(projectId, caller) → bool    // view
├── canManageBounty(projectId, caller) → bool    // view
├── getProject(projectId) → (owner, name, description, radicleIds[])
└── getRadicleIds(projectId) → string[]
Events:
ProjectCreated(projectId, owner, radicleId, name, description)
RadicleIdAdded(projectId, radicleId)
MaintainerAdded(projectId, maintainer)
MaintainerRemoved(projectId, maintainer)
AllowlistUpdated(projectId, wallet, allowed)
OwnershipTransferred(projectId, previousOwner, newOwner)
Permission model:
ActionWho can call
createBounty (on BountyEscrow)Owner, maintainer, or allowlisted wallet
acceptContribution / cancelBountyOwner or maintainer only
addMaintainer, updateAllowlist, etc.Owner only

BountyEscrow

Holds and distributes ETH rewards for completed work. Reads permissions from ProjectRegistry at call time — no stale per-bounty maintainer state.

Bounty struct:
struct Bounty {
  address creator;
  uint256 projectId;     // onchain ProjectRegistry ID
  uint256 rewardAmount;  // promised reward in wei
  uint256 fundedAmount;  // actual ETH held in escrow
  BountyStatus status;   // OPEN | FUNDED | ACCEPTED | CANCELLED
}
Functions:
BountyEscrow
├── createBounty(projectId, rewardAmount) → bountyId   // owner/maintainer/allowlisted only
├── fundBounty(bountyId) payable                       // anyone
├── acceptContribution(bountyId, contributor, patchId) // owner/maintainer only
├── cancelBounty(bountyId)                             // owner/maintainer only
└── getBounty(bountyId) → Bounty
Events:
BountyCreated(bountyId, creator, projectId, rewardAmount)
BountyFunded(bountyId, funder, amount)
ContributionAccepted(bountyId, contributor, patchId, rewardAmount)
BountyCancelled(bountyId, caller, refundAmount)
Key behaviours:
  • createBounty reverts with NotAuthorized if the caller is not the project owner, a maintainer, or on the allowlist
  • acceptContribution and cancelBounty revert with NotAuthorized if the caller is only allowlisted (not owner or maintainer)
  • fundBounty is unrestricted — anyone can contribute ETH to any open bounty
  • On acceptContribution, the full fundedAmount is transferred to contributor; on cancelBounty, it is refunded to creator
  • Both use ReentrancyGuard

Deployment

Deploy in order — BountyEscrow constructor requires the ProjectRegistry address:

# 1. Deploy ProjectRegistry
forge script script/DeployProjectRegistry.s.sol \
  --rpc-url $SEPOLIA_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY \
  --broadcast
 
# 2. Deploy BountyEscrow (reads PROJECT_REGISTRY_ADDRESS from env)
forge script script/DeployBountyEscrow.s.sol \
  --rpc-url $SEPOLIA_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY \
  --broadcast

After deploying, update all four contract address vars in .env and in your Railway environment:

PROJECT_REGISTRY_ADDRESS=0x...
PROJECT_REGISTRY_START_BLOCK=...
BOUNTY_ESCROW_ADDRESS=0x...
BOUNTY_ESCROW_START_BLOCK=...
NEXT_PUBLIC_PROJECT_REGISTRY_ADDRESS=0x...
NEXT_PUBLIC_BOUNTY_ESCROW_ADDRESS=0x...