Governance timelock + dispute resolution
Two changes that turn the contract from working into operationally credible: parameter changes can no longer be unilateral, and QA decisions are no longer terminal. Both shipped in the same release because they answer the same governance question — what stops the trusted role from acting in bad faith?
Governance timelock
Before v0.3.0, the OEM could rotate keys, change pricing, swap the ERS (Engineering Requirement Spec) reference, or upgrade the contract instantly. That was fine while the contract was a single-tenant prototype. It is not fine for a settlement layer that handles factory payouts. v0.3.0 routes those mutations through pending-update queues with enforced delays.
New types
| Struct | Purpose |
|---|---|
| PendingERSUpdate | Queues an ERS hash change with a configurable delay (min 1h) |
| PendingPricingUpdate | Reserved for cloud/customer-hosted fee schedule changes |
| PendingRolesUpdate | Reserved for OEM/Factory/QA address rotation |
| PendingUpgradeUpdate | Reserved for contract code upgrades (Wasm hash) |
| PendingFeeRateUpdate | Hard-coded 30-day delay; the longest timelock in the system |
Constants
const MIN_TIMELOCK_DELAY: u64 = 3600; // 1 hour floor
const FEE_RATE_TIMELOCK_DELAY: u64 = 86400 * 30; // 30 days for fee changes
const DEFAULT_CLOUD_FEE_BPS: u32 = 75; // 0.75%
const DEFAULT_CUSTOMER_FEE_BPS: u32 = 50; // 0.50%
Flow
Three-step proposal pattern, applied uniformly to fee-rate and ERS changes that shipped in this release:
1. propose_fee_rate(caller, new_bps)
→ queues a PendingFeeRateUpdate, eta = now + 30 days
2. approve_fee_rate(caller)
→ second-signer approval (multi-sig discipline)
3. execute_fee_rate(caller)
→ reverts if now < eta; otherwise applies the new rate
The 30-day fee-rate delay is intentional: factories quoting bids today need to know the fee they'll be paying when the order settles. Shorter delays would let an OEM front-run a known queue of pending settlements. ERS updates use the 1-hour floor because they're caller-supplied per-proposal — a factory that disagrees with a hot-swap can simply not accept the next service order during the delay window.
Dispute resolution
v0.2.0 added QA staking. v0.3.0 adds the path that actually puts a stake at risk: an OEM (or any caller paying the fee) can challenge a QA decision and trigger a re-inspection. If the re-inspection confirms the challenge, a portion of the QA's stake is slashed.
New functions
| Function | Caller | What it does |
|---|---|---|
| request_reinspect | OEM (or sample owner) | Pays the challenge fee, locks the lot, sets due_by for QA response |
| qa_reinspect_respond | QA | Submits revised counts within due_by; late responses revert |
| challenge_default_slash | OEM | If QA fails to respond by due_by, slashes up to MAX_SLASH_BPS |
Constants
const CHALLENGE_COST_BPS: u32 = 10; // 0.1% of lot value
const MAX_SLASH_BPS: u32 = 2000; // 20% cap on a single slash
const MAX_CHALLENGE_SAMPLE: u32 = 100; // re-sample size cap
The numbers are deliberately asymmetric. CHALLENGE_COST_BPS = 10 (0.1%) makes challenging cheap enough that a legitimately wronged OEM doesn't think twice; MAX_SLASH_BPS = 2000 (20%) caps the punishment so a single dispute can't bankrupt a QA running parallel jobs. Stakes are designed to absorb 5+ disputes before requiring re-collateralization — that's the operational floor the staking parameters were tuned against.
Slashing distribution
Inherited from v0.2.0: 50% of the slash goes to the challenger (refunding their fee plus an integrity bounty), 50% goes to the OEM (compensating for the disputed lot). The contract holds nothing — the platform doesn't profit from disputes.
Why these two together
Governance timelock and dispute resolution are the same primitive applied at two layers: delay before irreversibility. Parameter changes get a timelock. QA decisions get a re-inspection window. Both narrow the surface where a single trusted party can finalize an outcome before anyone else can react.
The lot-based pipeline (create_lot → qa_commit* → execute_payment) is unchanged. Existing lots continue to settle on the original path; new lots opt in to the dispute window simply by virtue of a non-zero QA stake being present.
Compatibility
Additive on top of v0.2.0. The new timelock applies prospectively — parameters set at init and never proposed-against retain their original behavior. Test suite grew to 47 passing (39 from v0.2.0 baseline + 8 new dispute/timelock tests), zero regressions.
Known follow-ups (closed in later releases)
qa_reinspect_respondinitially accepted late responses → closed in v0.4.1 with anow > due_byguard that reverts.challenge_default_slashinitially allowed QA to slash itself, and acceptedslash_bps == 0as a no-op → both closed in v0.4.1 (caller-must-not-be-QA + non-zero param check).PendingPricingUpdate/PendingRolesUpdate/PendingUpgradeUpdatedefined as types in v0.3.0; full propose/execute paths remain reserved for a later release.
References
Source: github.com/fairb-dev/Fairfoundry. Architecture diagrams + the full contract API table are on the platform page. The dispute hardening that closed the v0.3.0 follow-ups is described in v0.4.0 / v0.4.1.