Skip to Content
Quorum contracts are live on Base Sepolia. Mainnet ships after external audit. Do not send real funds.
ProtocolBonding

Bonding

BondingEscrow is where FOR and AGAINST stakes live for the lifetime of a bounty. AGAINST stake doubles as vote weight — there is no unbond. The escrow conserves tokens modulo bounded dust and settles atomically with ForumExecutor.

Surface

function bondFor(uint256 bountyId, uint256 amount) external; function bondAgainst(uint256 bountyId, uint256 amount) external; function settle(uint256 bountyId, bool forWon) external; // onlyForumExecutor function claim(uint256 bountyId) external; // anyone after settlement function flushProtocolCut(uint256 bountyId) external; // anyone after settlement function getBounty(uint256) external view returns (BountyState memory); function getPosition(uint256 bountyId, address user) external view returns (Position memory);

BountyState packs into two storage slots:

struct BountyState { address ideaToken; // 20 bytes uint128 totalFor; // 16 bytes uint128 totalAgainst; // 16 bytes Settlement settlement; // 1 byte (Open / ForWon / AgainstWon) bool protocolFlushed; // 1 byte }

Stake math

When settle(bountyId, forWon) is called by ForumExecutor, the loser pool is split:

  1. Protocol cut: protocolSlashBps × loserPool / 10000 (default 1000 bps = 10%) → treasury.
  2. Winner pool augment: loserPool − protocolCut → distributed pro-rata among winners.

Each winner’s payout:

share = winner.stake / totalWinner gross = totalLoser × share net = gross × winnerBps / 10000 // winnerBps = 10000 − protocolSlashBps payout = winner.stake + net

A worked example from the Sepolia E2E run:

QuantityAmount
Bounty amount10,000 idea tokens
FOR stake (winner)5,000
AGAINST stake (loser)2,000
Protocol slash (10% of loser)200 → treasury
Winner pool augment1,800 → FOR-bonder
FOR claim5,000 + 1,800 = 6,800
Bounty payout to claimant9,500 (95% after 5% fee)
Bounty fee → treasury500

Both halves of the protocol fee (bounty fee + slash fee) accrue to the protocolTreasury address configured on BondingEscrow and ForumExecutor. On mainnet this must be a Safe multisig with an explicit owner key rotation policy.

Why no unbond

AGAINST stake = vote weight. If unbonding were allowed, an attacker could:

  1. Bond AGAINST during the review window.
  2. Vote with the stake’s weight.
  3. Unbond immediately after voting.
  4. Re-bond on a new bounty, repeating the same capital.

Sybil-like vote amplification with the same capital. The fix is the strongest possible: the stake is locked from the moment of bonding until settle is called. FOR stake has the same restriction for symmetry (and because the settlement function is shared).

The downside: capital efficiency is low. A bonder commits tokens for the full review window plus any dispute cycles. This is intentional — it’s the cost of voting weight.

Settlement timing

settle(bountyId, forWon) can only be called by the ForumExecutor address. The call happens inside ForumExecutor._approve or _reject, which is invoked from finalize(bountyId).

Once settlement != Open, every winner can claim(bountyId) and the protocol can flushProtocolCut(bountyId). Losers receive nothing; their stake is consumed.

The settle call also writes protocolFlushed = false initially. A subsequent flushProtocolCut transfers the slash to treasury and flips protocolFlushed = true.

Dust and rounding

Each winner’s payout uses two sequential Math.mulDiv calls:

uint256 grossShare = Math.mulDiv(totalAgainst, p.forStake, totalFor); uint256 netShare = Math.mulDiv(grossShare, winnerBps, 10_000);

Each rounds down independently. Across N winners, the residual is bounded by 2 × N wei (the floor error of each mulDiv). The audit (M-01) recommends collapsing to a single mulDiv plus a rescueDust admin function.

In practice on mainnet: a bounty with 100 winners and a $1M loser pool leaves at most ~200 wei of idea tokens locked permanently. Negligible per-bounty; accumulates over the lifetime of the protocol.

Reentrancy + token shape

BondingEscrow uses ReentrancyGuard on every state-mutating external entry point and SafeERC20 on every token transfer. The reentrancy surface is closed.

The token shape surface is open: any ERC-20 can be a bounty token because ForumExecutor.createBounty accepts any address. The audit (H-02) recommends restricting bounty tokens to ChamberRegistry.getIdea(token).exists == true — i.e. only Quorum-graduated tokens. Fee-on-transfer and rebasing tokens otherwise break the conservation invariants.

Configurable parameters

ParameterDefaultBoundsOwner action
protocolSlashBps1000 (10%)0–3000 (max 30%)setProtocolSlashBps(uint16)
protocolTreasurydeployernon-zerosetProtocolTreasury(address)
forumExecutor(constructor)non-zerosetForumExecutor(address)

All three are owner-gated. The audit (M-04) flags that there is no time-lock; a compromised owner key can redirect treasury / executor instantly. Mainnet topology must use a Safe multisig + TimelockController (24h or 48h).

Last updated on