QA staking + slashing
v0.1.0 made QA a role on the contract. v0.2.0 makes it an accountable role. The QA address now posts collateral up-front, locks a portion of it for every open challenge, and forfeits a portion of it on default. Three small primitives — stake, lock-on-challenge, slash — that the v0.3.0 dispute layer would later compose into a full re-inspection flow.
The collateral
New mutators
| Function | Caller | What it does |
|---|---|---|
| stake_qa | QA | Posts collateral; tracked in State.qa_stake |
| request_unstake_qa | QA | Queues a withdrawal; cannot drop below min_qa_stake |
| execute_unstake_qa | QA | Releases the queued amount after QA_UNSTAKE_DELAY |
Constants
const QA_UNSTAKE_DELAY: u64 = 86400 * 7; // 7 days
const MAX_SLASH_BPS: u32 = 2000; // 20% cap on a single slash
The 7-day unstake delay matters: a QA who tries to exit immediately after submitting a contested decision can't dodge the slash. Their stake is exposed for the full duration of the dispute window, even if they've signaled an intent to withdraw.
min_qa_stake itself is configurable at init, so the OEM tunes the collateral floor to the lot sizes they expect that QA to inspect. A QA inspecting $10K lots needs less skin in the game than one inspecting $1M lots.
Lock-on-challenge
When a challenge opens against a lot the QA approved, 20% of min_qa_stake is locked into the lot's challenge record. The QA cannot withdraw that portion until the challenge resolves. Multiple concurrent challenges stack — five open challenges lock 100% of the floor, blocking all unstake.
// On challenge open:
let stake_to_lock = safe_mul(&env, st.min_qa_stake, 20) / 100;
// On challenge resolution (slash or no-slash):
let stake_to_unlock = safe_mul(&env, st.min_qa_stake, 20) / 100;
The slash
If a challenge resolves against the QA, the contract slashes up to MAX_SLASH_BPS (20%) of the locked portion. The slash distribution is fixed:
let challenger_reward = slash / 2;
let oem_reward = slash - challenger_reward;
Half goes to the challenger, half to the OEM. The platform takes nothing. This matters for two reasons: (1) the challenger's reward refunds their challenge fee plus an integrity bounty, so flagging legitimate fraud is profitable; (2) the OEM's reward compensates them for the disputed lot, so a successful challenge actually makes the wronged party whole rather than just punishing the bad actor.
The 20% cap is intentional. A QA running 10 parallel inspections shouldn't be wiped out by one bad call — the system absorbs an honest mistake without ejecting the QA from the network. Only sustained bad behavior across multiple disputes drops their stake below min_qa_stake, which then blocks them from accepting new lots.
Anti-grief: per-address rate-limit
Stake-and-slash systems invite a parallel attack: spam-challenge an honest QA to drain their locked stake even if every challenge eventually resolves no-slash. The contract caps challenge submission at 5 per hour per address:
// Allow max 5 challenges per hour
if count >= 5 {
return false;
}
This is a soft per-address ceiling, not a global rate-limit — coordinated attackers across many addresses can still apply pressure, but each address pays the 0.1% challenge fee per attempt, so the economics push the right direction. The combination of fee per challenge + per-address rate-limit + capped slash means a grief campaign costs the attacker more than it costs the QA.
What v0.2.0 does not ship
v0.2.0 wires the staking + slashing primitives but does not yet provide an on-chain dispute path that calls them in production. request_reinspect, qa_reinspect_respond, and challenge_default_slash arrive in v0.3.0. v0.2.0 lays the financial groundwork; v0.3.0 turns the key.
Compatibility
Additive on top of v0.1.0. The original lot lifecycle (create_lot → qa_commit* → execute_payment) is unchanged. Existing OEMs can deploy v0.2.0 with min_qa_stake = 0 to opt out of staking entirely, then raise the floor when their QA partner is ready to post collateral. Test suite grew to 39 passing (30 from v0.1.0 baseline + 9 new staking/slashing tests), zero regressions.
References
Source: github.com/fairb-dev/Fairfoundry. The dispute path that consumes these primitives is in v0.3.0. The architecture diagrams + full contract API table are on the platform page.