- rate limits on swap amount per time period
- max size per swap
- shut down if there’s a settled oracle price that’s far away from true price
- filter for tight max slippage
- shut down during very high volatility (>0.1% on a 4-second timeframe for ETHUSD)
Basic swap life cycle
openSwap is an RFQ-like system. The current contract has three on-chain phases:- Propose — swapper calls
propose(), depositingsellAmtofsellTokeninto the openOracle internal balance ledger and locking three ETH gas budgets (matcherGasComp,executorGasComp,settlerReward). - Match — matcher calls
matchSwap(), funding the oracle game as initial reporter and committingminFulfillLiquidityofbuyToken. The oracle game starts at this point. Matcher earnsmatcherGasComp. - Execute — once the oracle game settles (or is settled in the same call), any caller calls
execute()to finalize the swap. Executor earnsexecutorGasComp.
- Cancel — before any match, the swapper can cancel anytime; after
expiration, anyone can cancel and earnmatcherGasComp. - Bail out — after
block.timestamp > start + maxGameTime, anyone can bail out an outstanding matched swap and earnexecutorGasComp; both parties get their original swap deposits back. This applies even if the oracle has already settled.
Funding model
Matchers fund entirely from their openOracle internal balance.matchSwap does not accept Permit2 transfers or msg.value from the matcher — initialLiquidity (sellToken), oracle amount2 (buyToken), and minFulfillLiquidity (buyToken) must all already be sitting in the matcher’s internal balance, with openSwap approved as a spender.
Typical setup:
- Deposit working capital (USDC, WETH, ETH) into the oracle once via
oracle.deposit(token, amount, beneficiary). - Approve openSwap with
oracle.approveInternal(openSwap, token, max). - Match swaps and play the oracle game without per-call token transfers.
address(0)) flows through the ledger identically to ERC-20s; no WETH wrapping is required.
Matching the swap
When aSwapCreated event arrives, decode the 237-byte packed payload into the ProposedSwap and MatcherPreimage structs (layout). Validate every field against your filters before calling matchSwap — the same struct values are what you’ll re-supply to the contract.
matchSwap is a combined action: in the same transaction, openSwap commits you as the swap’s counterparty and openSwap calls oracle.report() on your behalf as the initial reporter. You are funding two positions at once — the swap’s minFulfillLiquidity of buyToken on the openSwap side, plus initialLiquidity of sellToken and amount2 of buyToken on the oracle side.
Once a SwapCreated event passes your filters, call:
matcherGasComp (credited to your openSwap-side tempHolding, claimable via withdraw).
The contract pulls funding from your openOracle internal balance:
initialLiquidityofsellTokenis committed as the oracle game’scurrentAmount1amount2ofbuyTokenis committed as the oracle game’scurrentAmount2— this is your initial report’s price (token1 / token2)minFulfillLiquidityofbuyTokenis held by openSwap to fulfill the swap at execute
amount2 is your choice; pair it with initialLiquidity to set the price you are publishing as the initial oracle report.
timing is the TimingBoundaries struct openSwap passes through to oracle.report() — bind to the block range you expect for the oracle game’s first report.
_swap and preimage must reconstruct the swap hash stored on-chain. The contract checks:
Executing the swap
To execute, you need three preimages reconstructed from events:MatchedSwap— decode the 207-bytepackedpayload from theSwapMatchedevent using the layout below.OracleGame— decode the latest 235-byte oracle preimage from openOracle’sReportSubmitted(initial) orReportDisputed(latest dispute) event for the swap’sreportId. The layout is documented in the openOracle Smart Contract Reference.PreimageHelper—{ reportId, creator, blockTimestamp, blockNumber }wherecreatoris the openSwap address (the contract creates oracle games on its own behalf duringmatchSwap), andblockTimestamp/blockNumberare pulled from theReportSubmittedevent’s block.
OracleGame.settlementTimestamp to the timestamp of the block containing ReportSettled before hashing. The other fields remain those of the last report.
If any of these don’t reconstruct the on-chain hashes, execute() reverts with WrongHash / WrongOracleHash, unless the settlement-timestamp adjustment described below matches.
Once you have all three, any address can call:
executorGasComp. If the oracle has not yet been settled, execute() settles it inline and the caller also receives settlerReward.
execute() then computes:
execute() refunds both sides if any of these conditions hold:
fulfillAmt > minFulfillLiquidityor the price is outside the slippage envelope: emitsSlippageBailout.- The
blocksPerSecondcheck fails: emitsImpliedBlocksPerSecondBailout. block.timestamp > start + maxGameTime: emitsMaxGameTimeBailout, even if the oracle settled before the deadline.
SwapRefunded. At exactly start + maxGameTime, the deadline does not force a refund. If none of these conditions hold, the contract transfers sellAmt to the matcher, fulfillAmt to the swapper, and returns (minFulfillLiquidity - fulfillAmt) as change to the matcher, then emits SwapExecuted.
The oracle hash and settlement-eligibility checks still run before the refund checks. An expired swap whose oracle is not settlement-eligible must use bailOut() instead. The executor earns executorGasComp on either successful execution or refund, plus settlerReward if this call settles the oracle.
looseTiming = true permits two settlement-timestamp retries if the supplied hash does not match:
- If the supplied
settlementTimestampis zero, try the current block timestamp. - If the supplied
settlementTimestampis greater than2, try the current block timestamp minus two seconds.
false, only the supplied preimage is checked.
Cancel and bail out
If the swap has not been matched yet:expiration, only the swapper can call this. After expiration, anyone can call it; the caller earns matcherGasComp, and the swapper recovers executorGasComp + settlerReward + sellAmt.
If the swap is still matched and block.timestamp > start + maxGameTime:
executorGasComp, the swapper gets back sellAmt, and the matcher gets back minFulfillLiquidity. Only the matched-swap preimage is needed; no oracle preimage or settlement eligibility is required. This emits SwapRefunded, without MaxGameTimeBailout. It does not settle the oracle; oracle funds follow the normal dispute and settlement flows.
Oracle parameters
The matcher’sMatcherPreimage carries the oracle game rules:
sellAmt. Reject swaps where the gas cost to correct a bad oracle price is too large relative to the initial liquidity, since that makes the oracle easier to bias.
escalationHalt should be between 2x and 3x sellAmt. This is the point where the multiplier stops growing but disputes can still continue at the halt size.
settlementTime is the per-round timer. Filter to a small set (e.g. exactly 4 seconds) so your volatility model only has to handle one window.
multiplier between 105 and 130 (1.05x to 1.3x) pairs reasonably with the 10%-of-sellAmt initial liquidity filter.
disputeDelay is the gap before a fresh report is disputable; lower values mean faster reaction but can intensify inclusion-race dynamics.
protocolFee is what disputers pay to the oracle game’s protocol-fee recipient. When nonzero, matching records a predicted per-swap oracleFeeReceiver address. Fees accrue to that address in the oracle’s internal ledger; the receiver need not be deployed yet. See Oracle fee distribution.
The swapper sets settlerReward in ProposedSwap; matching copies it into OracleGame. It is not a field of MatcherPreimage. The oracle game uses flags = 1 (time-mode = seconds) and no callback contract; the matcher cannot configure those.
Oracle fee distribution
execute() does not distribute accrued oracle protocol fees. Anyone can call:
sellToken as token1 and buyToken as token2, with its swapper and matcher addresses. The call deploys the predicted receiver if needed, then splits its accrued fees equally into the swapper’s and matcher’s oracle internal balances. An odd token unit goes to the matcher. It can be called again if more fees accrue; an empty distribution is a no-op.
When fees are distributed, the receiver emits:
Fulfillment fee parameters
The escalating fulfillment fee is determined by theMatcherPreimage fields maxFee, startingFee, roundLength, growthRate, and maxRounds.
startingFee at swap creation and increases multiplicatively each roundLength seconds until either maxFee or maxRounds is reached. The fee timer anchor is startFulfillFeeIncrease, set to the propose block timestamp by the contract; it is part of the preimage hash and is included in the SwapCreated packed payload.
Walking a simple example starting at timestamp 1000, with startingFee = 0.01% (1000), maxFee = 0.02% (2000), growthRate = 1.2x (12000), maxRounds = 3, roundLength = 4 seconds:
- 1000: 0.01%
- 1002: 0.01%
- 1004: 0.012%
- 1006: 0.012%
- 1008: 0.0144%
- 1010: 0.0144%
- 1012: 0.01728%
- 1014: 0.01728%
- 1016: 0.01728%
maxRounds parameter, which kicks in before the theoretical maxFee.
Same parameters but maxFee = 0.1% (10000), growthRate = 2x, maxRounds = 8, roundLength = 2:
- 1000: 0.01%
- 1002: 0.02%
- 1004: 0.04%
- 1006: 0.08%
- 1008: 0.1%
- 1010: 0.1%
maxFee cap binds before maxRounds.
The exact contract logic:
Quick note on slippage
The swapper setspriceTolerated and toleranceRange at propose time; you accept the swap later. The market may drift while you wait for the fulfillment fee to escalate. If the envelope is breached at execute time, both sides are refunded their original amounts.
Dynamic slippage
Oracle game costs are proportional to volatility, and bribery-style drain attempts can attach significant capital to an oracle game to push a bad price through. A reasonable approach is to let your accepted-slippage envelope be a function of prevailing market volatility, for examplemax(0.5%, 7 * settlementTime volatility). As the dispute network gets harder to game, this becomes less necessary.
How much of a fulfillment fee to demand?
Because the matcher submits the initial oracle report, the fulfillment fee must cover more than matching gas alone. At a minimum, your demanded fulfillment fee should cover:- expected oracle price bias if you want to be passive after matching
- expected initial reporter loss in the oracle game
- gas costs in excess of
matcherGasCompand (for self-execute)executorGasComp
0.65 * initialLiquidity * settlementTime volatility
where settlementTime volatility is the realized or estimated volatility of the sellToken / buyToken ratio over the oracle settlement horizon.
If you intend to be a mostly passive matcher, also size for expected oracle price bias from linear notional manipulation. A heuristic for that term is something like 30% of settlementTime volatility applied to swap notional, contingent on the multiplier and initialLiquidity filters being met.
So in practice the fulfillment fee floor is:
passive price-bias term + expected initial reporter loss + gas costs in excess of matcherGasComp + executorGasComp
rather than a simple volatility multiple.
Reserving tokens to dispute
You can reservesellToken and buyToken capacity in your oracle internal balance to dispute reports that move against you. SwapMatched’s packed payload includes reportId; only dispute reports tied to swaps you matched, so your capital isn’t tricked into an unrelated game.
A walkthrough of the disputing flow lives in the Smart Contract Reference. If you are following the safety precautions at the top of this page, active disputing is optional.
Core events
SwapCreated, SwapMatched, SwapExecuted, SwapRefunded, SwapCancelled, SlippageBailout, ImpliedBlocksPerSecondBailout, and MaxGameTimeBailout are the matcher-relevant swap events.
SwapCreated and SwapMatched carry tightly packed bytes from the contract’s _packMem helper rather than ABI-encoded fields. The packed payload of SwapCreated reconstructs the ProposedSwap + MatcherPreimage structs (the swap and oracle preimages committed at propose). The packed payload of SwapMatched reconstructs the MatchedSwap struct. Matchers and indexers need a custom decoder; openSwap stores only the hash.
SwapCreated packed layout
237 bytes total. The first 172 bytes are theProposedSwap, the next 65 bytes are the MatcherPreimage.
SwapMatched packed layout
207 bytes total, reconstructingMatchedSwap.
Struct definitions
The decoded packed payloads correspond to the structsmatchSwap, cancelSwap, execute, and bailOut expect as calldata:
Field notes
swapId is the unique identifier of the swap, indexed inSwapCreated/SwapMatched.
sellAmt is the amount of sellToken the swapper sells.
sellToken / buyToken are the token addresses. address(0) represents native ETH on either side. You can restrict yourself to swaps where one side is native ETH or native USDC, since that is what the MEV bot network currently supports.
minFulfillLiquidity is the amount of buyToken the matcher commits at match time. The matcher should accept swaps with enough minFulfillLiquidity buffer. If at execute time the oracle-implied fulfillAmt exceeds minFulfillLiquidity or the slippage envelope is breached, both parties are refunded their original amounts.
expiration is an absolute timestamp; matchSwap reverts if block.timestamp > expiration. Swappers also use this as the boundary for solo cancel.
priceTolerated and toleranceRange are the slippage envelope. priceTolerated is encoded as oracleAmount1 * 1e30 / oracleAmount2, where token1 is sellToken and token2 is buyToken. For example, ETH→USDC at $3000/ETH gives priceTolerated ≈ 1e18 * 1e30 / (3000 * 1e6) ≈ 3.33e38. toleranceRange is scaled in 1e7: 100000 is 1%, and the contract permits values up to 1e7. The accepted price bounds are floor(priceTolerated * 1e7 / (1e7 + toleranceRange)) through floor(priceTolerated * (1e7 + toleranceRange) / 1e7), inclusive. A price outside these bounds refunds both sides.
matcherGasComp, executorGasComp, and settlerReward are wei amounts the swapper deposits at propose. matcherGasComp is paid to the matcher in matchSwap. executorGasComp is paid to whoever calls execute() (often the matcher, sometimes a third party). settlerReward is the openOracle settler reward; if execute() also settles the oracle, the executor receives this on top of executorGasComp. In practice settlerReward is usually set to zero because execute() handles the oracle settle inline, which is more gas-efficient than running them as separate calls.
useInternalBalances is the swapper’s funding flag — when true, sellToken for the swap comes from the swapper’s openOracle internal balance instead of from a Permit2 transfer (ERC-20) or msg.value (ETH).
maxGameTime is the swap deadline in seconds since match. After block.timestamp > start + maxGameTime, anyone can call bailOut() on an outstanding matched swap, and execute() refunds instead of filling. The oracle may already have settled. The contract requires 20 * settlementTime <= maxGameTime <= 1 week. Filtering to between 2 minutes and 30 minutes is acceptable, provided the lower bound is met.
blocksPerSecond is the implied block rate used in an integrity check at execute time. On Base, the canonical value is 500 (0.5 blocks per second). The check measures elapsed blocks and seconds from the latest oracle report to execution. If they deviate too far from the configured rate, the swap refunds. You should reject any proposed swaps with blocksPerSecond != 500 on Base unless the block time changes.