Providing liquidity

Liquidity provision (matching orders) in openSwap is a complex game. While we are able to abstract the complexity down to a simple UI for swappers, there is no such simplification for matchers. To start, here are a few important safety guardrails: 1. rate limits on swap amount per time period 2. max size per swap 3. shut down if there's a settled oracle price that's far away from true price 4. filter for tight max slippage 5. 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, so there are multiple steps to each swap. 1. Swap is proposed, fulfillment fee starts escalating 2. Liquidity provider matches swap, locking in fulfillment fee. Bounty to oracle game initial reporter starts escalating 3. Initial reporter claims bounty, oracle game plays out and eventually settles 4. Upon settlement, swap is executed at the final oracle price.

Core events

When a swap is created, you will see the following event:

    event SwapCreated(uint256 indexed swapId,
                      address indexed swapper,
                      uint256 sellAmt,
                      address sellToken,
                      uint256 minOut,
                      address buyToken,
                      uint256 minFulfillLiquidity,
                      uint256 expiration,
                      uint256 priceTolerated,
                      uint256 toleranceRange,
                      FulfillFeeParams fulfillFeeParams,
                      uint256 blockTimestamp,
                      OracleParams oracleParams,
                      BountyParams bountyParams,
                      uint256 gasCompensation);

swapId is the unique identifier associated with the created swap.

sellAmt is the amount of sellToken the swapper wants to sell.

sellToken is the address of the token the swapper wants to sell.

minOut is the minimum amount of buyToken the swapper can receive.

buyToken is the address of the token the swapper wants to buy.

If buyToken or sellToken is address(0), this represents native ETH. You can restrict yourself to swaps where buyToken or sellToken is one of address(0) or native USDC, since that is what the MEV bot network currently supports.

minFulfillLiquidity is the amount of of buyToken the matcher must put up to match the swap. Generally, it is recommended to demand an ~5% buffer above the USD value of the sellAmt to ensure the swap doesn't get refunded. If there isn't enough liquidity to execute the swap, both parties get the original amounts back. The openSwap UI is programmed for swaps to include this buffer in minFulfillLiquidity, but this doesn't mean all SwapCreated events will be like this.

expiration is the time period, in seconds, after a swap is matched without an oracle game initial report where the swapper and matcher can cancel and get their tokens back.

priceTolerated and toleranceRange are the core slippage parameters. priceTolerated is in units of 1e18 * sellToken / buyToken, where the ratio of sellToken to buyToken is the current price. For example, if sellToken were ETH and buyToken were USDC, and 1 ETH = 3000 USDC, a priceTolerated of 1e18 * (1e18 / 3000e6) would be accurate. toleranceRange is the max deviation from priceTolerated allowed by openSwap, in 1e7 units. Here, a toleranceRange of 1000 implies a 0.01% max deviation from priceTolerated. If the toleranceRange around priceTolerated is violated, the swap refunds (both participants get their original amounts back). For safety it is recommended to filter swaps for priceTolerated and toleranceRange that leave you no more than 0.5% away from the slippage barrier on the side that hurts you. If a proposed swap does not match that filtering, reject it.

fulfillFeeParams represents the escalating fulfillment fee parameters which has its own dedicated section below.

blockTimestamp is simply the block.timestamp of the block the event is in. oracleParams represents the openOracle game parameters which has its own dedicated section below.

bountyParams represents the initial reporter bounty contract parameters which has its own dedicated section below. gasCompensation represents the amount in wei you as a matcher are compensated with in exchange for calling the matchSwap() function. This is paid for by the swapper. The matchSwap function is ~1 million gas. If a swap has been matched, you will see the Swap Matched event, and you can no longer match that specific swapId:

Fulfillment fee parameters

The fulfillFeeParams object in the SwapCreated event looks like this:

As a matcher, you want to match the swap once you determine the fee you receive is worth it. The fee you receive starts at startingFee as soon as a swap is created and increases up to maxFee over time. The fee you earn depends on the time you match it. We can walk through a simple example, starting at timestamp 1000 (startFulfillFeeIncrease), noting blocks on Optimism are 2 seconds each. We'll set startingFee to 0.01% (1000 in contract units), maxFee to 0.1% (2000 in contract units), growthRate to 1.2x (12000 in contract units), maxRounds to 3, and roundLength to 4 seconds. timestamp: fee if matched at that time 1000: 0.01% 1002: 0.01% 1004: 0.012% 1006: 0.012% 1008: 0.0144% 1010: 0.0144% 1012: 0.0144% 1014: 0.0144% Note how the fee tops out at 0.0144% (3 rounds of escalation) because of the maxRounds parameter, which is well below the theoretical maxFee of 0.1%. The fee updates every 4 seconds because roundLength is 4. Next, what if we use the same parameters except growthRate is 2x, maxRounds is 8, and for simplicity roundLength is 2? 1000: 0.01% 1002: 0.02% 1004: 0.04% 1006: 0.08% 1008: 0.1% 1010: 0.1% 1012: 0.1% 1014: 0.1% 1016: 0.1% The fee tops out at the maxFee of 0.1% in advance of hitting maxRounds. The exact contract logic for the fee a matcher earns is as follows:

Quick note on slippage

Since the swapper sets the slippage parameters at time of swap creation, and you accept the swap later, the market may drift while you wait for the fulfillment fee to increase enough to be worth it. It's more of a cost to the swapper than the matcher if slippage is breached, since you as as matcher get your original funds returned, while the matcher paid the initial reporter in the oracle game as well as gas compensation to you as the matcher. It is up to you if you want to match swaps that are close to the priceTolerated walls when the fulfill fee is worth it to match.

Oracle parameters

Once you match a swap, the oracle game is kicked off. The rules of the game are determined by the oracleParams object in the SwapCreated event:

Filtering is very important here. If you agree to swap with bad oracle game parameters, you can lose money. Note the gas fee estimation should include L2 gas fees, tips for inclusion as well as the L1 gas component on Optimism. settlerReward is how much the oracle game settler gets paid, in wei. This should be greater than the cost ~500k of gas based on prevailing gas fees.

initialLiquidity should be at minimum 10% of the sellAmt in the SwapCreated event. You should also reject swaps where 250k gas at prevailing gas fees is > ~0.0075% of initialLiquidity. This is to prevent situations where it isn't profitable to correct oracle game prices because of gas, since a dispute in the oracle game costs ~250k gas.

escalationHalt should be between 2 and 3 times sellAmt from the SwapCreated event. This is where the oracle game stops escalating by the multiplier each round but disputes can continue at the same size.

settlementTime is the per-round timer in the oracle game. If you filter timeType to True, you can reject all swaps where settlementTime is not 4. The longer the settlementTime, the less accurate the final oracle price. latencyBailout is when you are allowed to cancel a swap after the match provided there has been no oracle game initial report yet, and is always measured in seconds. Filtering to <= 30 seconds is fine. maxGameTime is an important parameter: you can cancel a swap if the oracle game takes this much time without a settlement. It is measured in seconds. Filtering maxGameTime to above 2 minutes but <= 30 minutes is acceptable. If set too low, your counterparty can cancel the swap before it completes, which is an annoyance even if there is no substantial loss to the matcher. If set too high and you accept bad oracle game parameters, it is possible to be griefed for the time value of your capital or lose the max slippage. The contract rejects any swaps with maxGameTime > 1 week. blocksPerSecond represents the block rate of the network. On Optimism, there's 1 block every two seconds, which is a block rate of 0.5 per second, represented in the openSwap contract as 500. Filtering to exactly 500 is necessary or no swaps will complete. We use this in an implied blocks per second sanity check upon execution, where we compare the change in block number to the change in timestamp to see if OP Mainnet was well-behaved in this aspect over the life of the swap. If not, both parties are refunded and no swap executes. disputeDelay represents how long the MEV bot network in the oracle game must wait to correct a mispriced report. Filtering to exactly 1 is acceptable here to prevent situations where someone can escalate the oracle game to its escalationHalt in a single block.

swapFee represents how much an MEV bot must pay the prior reporter in the oracle game when disputing. A swap fee of 0.01% is represented as 1000 in contract units. You generally want a very low swap fee in the oracle game to ensure accurate pricing. Filtering to exactly 1 is necessary here, as that is the minimum swap fee openOracle supports.

protocolFee represents how much an MEV bot pays the protocolFeeRecipient in the oracle game if they want to dispute a report. A protocol fee of 0.01% is represented as 1000 in contract units. In openSwap, the protocolFeeRecipient is a special contract that splits accrued protocol fees between the swapper and matcher at time of swap execution. It is recommended to filter protocolFee to <= 250. While 250 (0.0025%) is small, it does increase the price error in the openOracle game, so you can incorporate it into your demanded fulfillment fee to match the swap by just adding the protocol fee to the fee you would have demanded otherwise. multiplier represents how much the oracle game size increases with each dispute. 130 means a multiplier of 1.3x each round. From some of our work on openOracle manipulation strategiesarrow-up-right, it seems like pairing the initial liquidity requirement above (min 10% swap size) with a 1.3x multiplier is reasonable, so you can filter the multiplier to exactly 130.

timeType is a bool that represents how time is kept in the oracle game. True means time is measured in seconds, false means blocks. You should filter timeType to True to keep things simple.

Bounty parameters

A bounty is paid to the first person to report a price in the oracle game. In openSwap, the swapper pays the bounty, and its structure is defined by the bountyParams object in the SwapMatched event:

As long as you are filtering to a sane latencyBailout like 30 seconds as discussed above, it is hard to get hurt very much by bad bounty parameters, since this just means nobody will come initial report. But, you want to match swaps that will have an initial reporter come play the oracle game so you can earn the eventual fulfillment fee. The bounty escalation math works exactly the same as the fulfillment fee escalation math above. startingFee is to bountyStartAmt as maxFee is to totalAmtDeposited. In openSwap, bounties are typically paid in either ETH (bountyToken of address(0)) or USDC (bountyToken of Native USDC), so you can filter to bountyParams with either of those as bountyToken. Generally, it is fine to filter to bountyMultiplier >= 11000, roundLength == 1, maxRounds == 20, and a bountyStartAmt that when multiplied by (bountyMultiplier/10000)^20 is > 0.1% of sellAmt, in USD, provided that totalAmtDeposited is ≥ bountyStartAmt * (bountyMultiplier/10000)^20. If totalAmtDeposited is less than this number, as long as it's > 0.1% of sellAmt in USD it is acceptable. This ensures it is very likely the bounty will get taken by an initial reporter within 20 seconds so you can eventually earn your fulfillment fee.

How much of a fulfill fee to demand?

From our work on openOracle manipulation strategiesarrow-up-right, it seems like ~30% of the settlement time volatility is a reasonable floor so long as the multiplier is ~1.3x and initial liquidity is ~10% of swap size. For example, if settlementTime is 4 seconds, you can calculate a standard deviation of a time series of 4-second returns and use that. protocolFee and gas fees as % initial liquidity should be included in your minimum demanded fulfillment fee as well. To the extent the swapping flow is not adversarial, you can reduce the demanded fulfillment fee significantly, though it is still recommended to follow the basic safety precautions outlined at the top of the page.

Reserving tokens to dispute

As a matcher, you can reserve some WETH and USDC tokens to play the oracle game and defend your position if desired. Once the swap is matched, you see the reportId of the oracle game that decides your fate:

You can program your matcher bot to only dispute reportIds that you are involved in, so as to not get tricked into depositing your tokens into an unrelated oracle game so they are unavailable for defense. A guide to disputing can be found herearrow-up-right. If you are following the safety precautions at the top of the page, this is not strictly necessary and adds possibly unneeded complexity.

Dynamic slippage

Since costs in the oracle game are proportional to volatility and people can attempt drain-bribery attacks where they pay all capital attached to the oracle game to deposit money into the contract and try to slip a bad price through, you can have your slippage tolerated (distance to slippage barrier that hurts you) be a function of prevailing market volatility. Something like max(0.5%, 7 * settlementTime volatility) may be acceptable. As the dispute network becomes harder to reason about, defenses like this become less necessary.

Matching the swap

Once you have determined the SwapCreated event parameters are enticing enough per above filtering rules, you can call the below smart contract function to match the swap:

You transfer in minFulfillLiquidity of sellToken and receive gasCompensation in wei upon calling the function.

paramHashExpected is calculated as follows:

The reason we use paramHashExpected is to protect again adversarial RPCs that lie to you about the chain state in order to get you to match a horrible trade. If the chain state doesn't match the param hash you calculate locally using data you've validated the economics of, the match reverts instead of torching capital. To make things more simple, we've provided the below python code example of how to calculate the param hash when matching a swap. The param hash depends only on the SwapCreated event data.

It is not recommended to use the getSwapHash() function in the smart contract unless you are running a full node, since this fully trusts the RPC to give you the hash corresponding to the SwapCreated event data.

A dangerous game

We would like to remind the reader that this is a dangerous game and high amounts of diligence and adherence to safety best practices are strongly recommended. If there are any questions at all, we are happy to answer them in telegram and discord.

Last updated