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:
- Protocol cut:
protocolSlashBps × loserPool / 10000(default 1000 bps = 10%) → treasury. - 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 + netA worked example from the Sepolia E2E run:
| Quantity | Amount |
|---|---|
| Bounty amount | 10,000 idea tokens |
| FOR stake (winner) | 5,000 |
| AGAINST stake (loser) | 2,000 |
| Protocol slash (10% of loser) | 200 → treasury |
| Winner pool augment | 1,800 → FOR-bonder |
| FOR claim | 5,000 + 1,800 = 6,800 |
| Bounty payout to claimant | 9,500 (95% after 5% fee) |
| Bounty fee → treasury | 500 |
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:
- Bond AGAINST during the review window.
- Vote with the stake’s weight.
- Unbond immediately after voting.
- 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
| Parameter | Default | Bounds | Owner action |
|---|---|---|---|
protocolSlashBps | 1000 (10%) | 0–3000 (max 30%) | setProtocolSlashBps(uint16) |
protocolTreasury | deployer | non-zero | setProtocolTreasury(address) |
forumExecutor | (constructor) | non-zero | setForumExecutor(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).