DR fee mutualizer
A dispute resolver (DR) charges a fee when an exchange is escalated. Two naïve options for who pays:
- Seller pays. Then any buyer can grief the seller by escalating frivolously.
- Buyer pays. Then buyers are discouraged from escalating legitimate complaints.
Neither is good. The DR fee mutualizer introduces a third option: an external mutualizer contract that pools DR fees. A seller pays a small premium per offer (or per-exchange-cap-per-period), and the mutualizer pays the DR fee from the pool if escalation happens. Buyers can escalate freely; sellers don't bear the unilateral risk; the mutualizer operator earns a margin via the premium spread.
The protocol speaks a minimal interface (IDRFeeMutualizer) — isSellerCovered, requestDRFee, returnDRFee. Anyone can deploy a mutualizer; the reference implementation IDRFeeMutualizerClient adds agreement management on top. A seller picks a mutualizer per offer at creation time (or self-mutualises by setting the address to zero).
Flow
requestDRFee is called by the protocol at commit time; if the mutualizer can't cover, the commit reverts (no buyer/seller funds get locked; no voucher issued; safe failure mode).
Self-mutualised offers
Setting DRParameters.mutualizerAddress to address(0) means the seller bears the DR fee themselves out of their treasury. This is the default and the right call for low-DR-fee offers (e.g. canonical free DR), high-trust contexts, or when no mutualizer is available.
Most offers in the protocol today are self-mutualised with feeAmount = 0, so the question is mainly academic until you use a paid DR.
When to use
- High-value offers with a paid DR — capped per-exchange fee + amortised premium is cheaper than the worst-case escalation.
- Marketplaces with broad seller bases — operators can run a mutualizer as a service.
- Sellers with predictable dispute rates — the math works out if your actual dispute rate is below the mutualizer's pricing.
Building a mutualizer
Implement IDRFeeMutualizer (the minimal interface) at a contract you control. You can also implement the optional IDRFeeMutualizerClient to expose admin/seller-facing agreement management — strongly recommended for ecosystem interop:
interface IDRFeeMutualizer is IERC165 {
function isSellerCovered(
uint256 sellerId,
uint256 feeAmount,
address tokenAddress,
uint256 disputeResolverId
) external view returns (bool);
function requestDRFee(
uint256 sellerId,
uint256 feeAmount,
address tokenAddress,
uint256 exchangeId,
uint256 disputeResolverId
) external returns (bool success);
function returnDRFee(uint256 exchangeId, uint256 feeAmount) external payable;
}The protocol calls requestDRFee from the address of ProtocolDiamond; verify the caller and revert on anyone else. returnDRFee is also Diamond-only.
For native-currency agreements, returnDRFee sends msg.value == feeAmount. For ERC-20s it pulls via transferFrom (so your mutualizer needs Diamond as an approved spender), or the Diamond pushes via transfer — implementations vary; check the reference.
IDRFeeMutualizerClient extends with:
deposit / withdraw— fund the pool.newAgreement / voidAgreement / payPremium— agreement lifecycle.getAgreement / getAgreementId— reads.
Each agreement specifies a seller, exchange-token, dispute-resolver triple (DR ID 0 = universal across all DRs), plus per-tx cap, total cap, time period, premium, and refundOnCancel. This shape covers the common business models without locking implementations into it.
Configure on an offer
await sdk.createOffer({
// …offer fields…
// DRParameters used to be just `disputeResolverId`; mutualizerAddress was added later.
drParameters: {
disputeResolverId,
mutualizerAddress: MY_MUTUALIZER_ADDRESS, // or 0x0 for self-mutualised
},
})Switching mutualizers later is allowed (mutualizer address is one of the few mutable offer fields). Existing exchanges keep their original mutualizer — the change only affects future commits.
Read coverage status before committing
A buyer or seller dApp should check whether the mutualizer will actually cover before letting the buyer commit, so the commit doesn't revert at the worst possible moment:
const mutualizer = new ethers.Contract(mutualizerAddress, MUTUALIZER_ABI, provider)
const covered = await mutualizer.isSellerCovered(
sellerId,
drFeeAmount,
exchangeToken,
disputeResolverId,
)
if (!covered) showWarning("Seller's mutualizer is out of funds; commits will revert")Common gotchas
- Pool drain risk. If a popular mutualizer runs dry, every commit on every offer pointing at it reverts. Subscribe to balance events or fall back to self-mutualisation.
- Premium economics. A mutualizer pricing assumes a known dispute rate. Bad actors can stack escalations against a single covered seller. Limit per-seller exposure (
maxAmountPerTx,maxAmountTotal). - Trust-bootstrapping. A new mutualizer with no track record is a hard sell. Operators typically front the float themselves until premiums catch up.
- One mutualizer, many sellers. Pool the risk widely so the law of large numbers actually kicks in.
- DR refusing arbitration returns the fee to the mutualizer (not the seller). The seller's premium is the cost of the option; if the DR refuses, the option's value was the unused coverage.