CrocSwap Whitepaper
Introduction
CrocSwap is an Ethereum-based automated market maker (AMM) that supports concentrated liquidity.
Unlike other decentralized exchange (DEX) protocols, CrocSwap consolidates the entire exchange into a single smart contract. Individual liquidity pools are represented as light-weight data structures inside the single-contract DEX. The collateral across every pool in the protocol is co-mingled at a single smart contract address.
Other major DEX protocols deploy separate smart contracts for every individual liquidity pool. CrocSwap's consolidation into a single smart contract allows users to execute complex multi-pool operations, and net the collateral flows across pools. This produces significant gas and tax savings by avoiding the unnecessary re-shuffling of tokens between multiple contracts in the protocol.
CrocSwap behaves like a constant function market maker (CFMM) for small local moves, but allows liquidity providers (LPs) to use range orders to provide liquidity on a pre-defined grid of prices. As the price in the AMM moves across ticks, liquidity is atomically added or removed based on the range orders bounded at those ticks. Concentrated liquidity allows liquidity providers to achieve significantly higher capital efficiency, earning the same fees on a much smaller amount of collateral. The tradeoff is concentrated LPs stop earning fees if/when the price moves out of range.
CrocSwap supports a dual liquidity model, where concentrated and classical "ambient" liquidity co-exist within the same underlying pool. This resolves two issues in existing concentrated liquidity AMMs. First, CrocSwap allows for fungible ambient LP tokens, without fragmenting liquidity. Second, the fees accumulated by CrocSwap LP positions instantly compound returns, without the use of burdensome manual "collect" calls in existing concentrated liquidity AMMs.
Within CrocSwap multiple fee tiers of liquidity pools can exist for every asset pair. These fee tiers are defined arbitrarily at the protocol governance level. Multiple tiers expose more options for LPs to customize orders based on their market views. During periods of low volatility and light trading, LPs in low fee tiers are expected to outperform. While they earn lower fees per amount traded, the sizable majority of trading will happen in low fee pools due to the cost advantage to the trader. In contrast, during periods of high market dislocation, price dislocation will tend to outsize the fee savings and LPs in high fee tiers will outperform.
Liquidity Curve
The fundamental primitive underlying every pool in the protocol is the liquidity curve. Within a local neighborhood of prices, a CrocSwap curve behaves like a standard constant-product AMM curve. A curve represents a market between two assets: a base token and a quote token. At any given time, a curve has a certain quantity of virtual reserves of each asset. The virtual reserves represent the amount of tokens that would have to be held by a fully collateralized constant-product AMM with equivalent price and liquidity.
The price of the curve at any given time is the ratio of virtual reserves of the base asset divided by virtual reserves of the quote asset. As the price changes the virtual reserves between the respective sides of the pair rebalances. Liquidity is the product of the virtual reserves on each side of the pair. Besides fee accumulation and rebalancing related to concentrated liquidity barriers, liquidity remains invariant across any swaps applied to the curve.
Concentrated Liquidity
Concentrated liquidity is the ability for liquidity providers (LPs) to add liquidity that's only active inside a given price range. This is done through the use of range orders which are LP positions tied to a fixed user-determined lower and upper price. As long as the curve's price is within this range, the position contributes liquidity to the pool's curve. The LP earns a pro-rata share of liquidity fees for the portion of any swap executing within this range. When price moves outside the range, the LP order is no longer active. The liquidity does not contribute to the curve, and the LP does not accumulate any fees as long as the price remains outside the range.
The real reserves represent the actual token collateral held by the pool. Because liquidity is concentrated, the real reserves will be equal or less than the virtual reserves. Collateral safety is maintained by re-adjusting curve liquidity, any time the price moves through a bump point. A bump point can either increase or decrease the total liquidity depending on the direction of the cross. A bump point exists at a price if and only if one or more existing range orders have a boundary at that point.
Ambient Liquidity
CrocSwap supports a hybrid system combining concentrated liquidity pioneered by Uniswap V3 with the constant-product liquidity found in other AMMs like Uniswap V2 or SushiSwap. CrocSwap refers to the latter type of liquidity as ambient liquidity. Ambient liquidity is active at every possible price point, never moves out of range, contributes liquidity to the curve as long as the order is active, and earns fees as long as the LP position remains open.

Internally ambient liquidity is tracked in terms of ambient seeds. At the initialization of the pool one unit of ambient seeds are convertible to exactly one unit of ambient liquidity. As the pool accumulates fees the value of ambient seeds deflates relative to liquidity (which is always defined in terms of )
From the curve's perspective, an ambient liquidity position is equivalent to a range order with lower and upper bounds set at the min/max possible price (therefore never goes out of range). There are two primary advantages to native ambient liquidity orders. First they involve much less internal book keeping and are therefore significantly more gas efficient to mint than range order LP positions.
Second, CrocSwap internally exposes the aggregated ambient liquidity on the curve separate from concentrated liquidity. That allows any external contract to easily measure total ambient liquidity and its proportion of active liquidity in a single view call.
One of the problems with Uniswap V3's model is that it breaks the widely used convention of fungible LP tokens. With CrocSwap pools it's trivially easy to construct ERC20-compliant LP tokens based on the ambient liquidity of an individual pool. Like Uniswap V2, such tokens automatically compound returns over time as accumulated fees inflate the value of resting ambient liquidity in the pool.
With ambient liquidity, pre-existing liquidity mining programs and protocol owned liquidity can be applied to CrocSwap pools with minimal modifications. Ambient LP tokens are also much easier to manage than concentrated liquidity NFTs. In contrast, ambient liquidity can be roughly approximated in Uniswap V3 through ultra-wide range orders, but it's impossible to aggregate this total liquidity in an on-chain context, such as liquidity mining. In contrast CrocSwap ambient can be committed and aggregated by reading a single on-chain value.
We therefore imagine that CrocSwap will allow for the seamless transition from low-touch ambient liquidity in the long-tail of assets to more actively managed concentrated liquidity in major pairs. That avoids the awkward fragmentation of liquidity currently seen between Uniswap V2 and Uniswap V3.
Fees & Rewards
Like most other AMMs, swap fees in CrocSwap are fixed as a percentage of the notional swapped. This number is called the fee rate. Some fraction (possibly zero) of these fees accumulate to the protocol governance as the protocol take. The rest accumulate as liquidity rewards to the LPs in the pool pro-rata based on the fraction of active liquidity that their position contributes at the time of the swap. Fee rate and protocol take vary depending on pool type.
Liquidity rewards in CrocSwap are directly assimilated into the curve as ambient liquidity. In contrast, Uniswap V3 accumulates rewards as dead capital. That requires the users to periodically collect and rebalance their accumulated rewards, which otherwise would sit idle earning no return. The collect action itself is expensive in terms of gas.
In CrocSwap, rewards are directly converted to ambient liquidity at the time they're accumulated. This results in the continuous compounding of accumulated rewards without the need for manually triggered gas-heavy collect operations found in Uniswap V3. A secondary effect is that concentrated liquidity continuously emits ambient liquidity to the pool over time, improving market quality and depth.
Finally, this improves gas efficiency for swap operations as only a single cumulative growth rate needs to be tracked (ambient liquidity) instead of two separate rewards rates (base and quote token growth rates).
Price Ticks
In CrocSwap, prices are sub-divided into a grid of ticks. Ticks are represented by a signed integer corresponding to where T is the tick index. The tick index can either be positive (price above 1.0), or negative (price ratio below 1.0). Therefore a one tick change corresponds to moving the price by one basis point (0.01%).

For bookkeeping reasons, range orders can only be placed at a price corresponding to a specific tick index. Different pool types will set a tick size represented by a whole number corresponding to a minimum usable spacing between tick indices. In practice, this is enforced by only allowing range orders where
(where T is the tick index and S is the tick size.)

Since bump points can only occur where range order boundaries exist, and range orders can only be placed on the tick grid, increasing tick size decreases the frequency of crossing bump points. Since crossing a bump point costs gas on the swap operation, a higher tick size increases the average gas efficiency of swaps, particularly large market moving swaps. However, setting a lower tick size increases the granularity of range orders and allows LPs to add more liquidity for the same amount of collateral.
For this reason, tick size is set individually and varies by pool type. Generally speaking, lower fees co-occur with smaller tick sizes. For example, it's likely that stablecoin pairs (e.g. USDC/USDT) will be set by the protocol authority with the smallest tick range possible. The CrocSwap protocol authority can change tick size after the pool has been created. If this occurs any pre-existing range orders on the old grid can be burned, but any newly minted orders must occur on the new grid.
Grid Improvement
The tradeoff between smaller and larger tick granularity is fundamentally economic. It is also dependent on the size of the underlying liquidity in the range order. If a large LP wants to add a lot of liquidity, but it's inside the minimum tick size, allowing them to do so can still be beneficial to swap users. That occurs when the liquidity is high enough to offer enough price improvement to offset the gas cost imposed by crossing a bump point.
Therefore CrocSwap allows for grid improvement when the size of the range order crosses a threshold. This threshold is set by protocol governance at the token level. Protocol governance will adjust this threshold over time depending on the price of gas and the exchange rate of individual tokens.

When minting a concentrated LP position with grid improvement, the user is free to choose whether to test against the base or quote token thresholds. (Users not minting an LP position off-grid, will select neither token and therefore avoid the gas overhead of the parameter lookup.) It's assumed that users will make the most advantageous choice based on off-chain logic.
Grid improvement is gated based on two pre-conditions. both thresholds are set on a per token basis.
- Size of real token reserves
- Distance from the current price tick.
Reserves Threshold
Because grid improvement is set by token, rather than pair, the size threshold is represented in terms of real token reserves instead of liquidity. However only the reserves committed to the off-grid segment of the range order counted towards the threshold. From the perspective of the swap users paying the gas cost to cross the additional bump point, only the reserves in the off-grid range segment matters. The on-grid liquidity (if any) does not create price improvement.
The reserve commitment scales depending if one or both of the boundary points of the range order occurs off-grid. The latter doubles the threshold requirement. (If neither boundary point is off-grid, by definition the order is on-grid and no off-grid threshold applies.)
Distance from Price
In general we want to avoid off-grid range orders placed too far from the current market price. Past a certain distance, the market price is unlikely to ever reach these ranges. (Imagine an off-grid order on ETH/USDC places at a price of $0.000001 or $1000000000.) Very distance range orders have very small expected value in terms of end-user price improvement.
Therefore off-grid range orders are thresholded based off the distance (in ticks) to the current price. At least one of the off-grid range order's boundaries must be within the threshold distance from the pool's current price tick.
Because only the distance to boundary points is compared, it's possible that an off-grid range order could fail the distance check, even if the market price tick is contained within the range order. However this is intended, since price improvement can only happen on the off-grid segment of the range order, which by definition must occur at the boundaries of the range order.
JIT Liquidity
Just-in-time (JIT) liquidity is a potential market quality issue specifically affecting concentrated liquidity AMMs. JIT liquidity occurs when a JIT trader sees a pending swap transaction that has not yet been committed on chain. The trader "flashes" in a large amount of concentrated liquidity into the pool, collects the lion share of fees generated by that trade) then flashes out their LP position shortly after. Possibly all within a single block.
Theoretically JIT liquidity is possible in classical ambient liquidity AMMs, but not practical. Liquidity fees are allocated based on an LP's pro-rata contribution of liquidity to the pool at the time of the swap. With ambient liquidity, this requires the JIT trader to hold in reserve more than the total the capital in the pool to capture 50% or more of a swap's fees. Concentrated liquidity allows traders to leverage small amounts of capital into large amounts of active liquidity, particularly if they are only active for a single swap and can narrow the price range exactly around the current price.
JIT allows traders to earn abnormally large returns on their capital with less risk than regular passive LPs. The JIT trader can selectively choose which trades to provide liquidity against. That opens up the possibility of "toxicity" for passive LPs in the pool, who eat the losses on high market impact trades, while JIT traders collect fees on low-impact and mean-reverting traders. For example a JIT trader may watch the real-time price discovery at a centralized exchange (CEX), and only provide when the AMM price is cheap relative to the CEX.
Market Quality
From a market quality perspective there are arguments for and against allowing JIT liquidity. From the perspective of any individual swap, the introduction of additional JIT liquidity. on top of passive liquidity improves execution quality. (An exception exists for swaps with tight limit price boundaries. In this case the presence of an informed JIT trader, tight limit price creates order toxicity for the swapper.)
From the perspective of passive LPs in the pool, JIT liquidity is a persistent cost. It represents the diversion of liquidity fees to another set of participants with who take much less risk and have less capital commitment. It reduces yields for passive LPs with no commensurate reduction in price risk.
In the long run, JIT liquidity may lead to an undesirable equilibrium where almost all passive LPs leave the pool, because JIT traders drive passive yields to below the risk-justified rate of return. The more passive LPs leave, the easier it is for a JIT trader to take more liquidity fees on any single swap. Ultimately this may results in an "RFQ market" where no firm quotes exist and swappers are forced to shoot transactions into the void, waiting for the JIT traders to give them an arbitrary price and size.
This is a particular risk on rollups or L1s where gas fees are small. The main constraint to JIT trading is the gas cost of adding, then burning an LP position. Since CrocSwap is designed to be robust within many on-chain environments it implements flexible controls at the governance level to deal with JIT.
JIT Mitigations
With the above in mind, CrocSwap allows for a flexible facility to restrict JIT liquidity. This can be turned on at either the protocol or the individual pool level. Each pool contains a universal JIT threshold parameter which applies to every concentrated LP position in the pool. (Ambient LP positions are ignored, because of the impracticality of JIT trading on ambient liquidity.)
The JIT threshold defines a minimum time-to-live (TTL) in seconds. When LP's are minted, the associated data structure is marked with the block timestamp of their creation. The LP position cannot be burned until the TTL interval has elapsed.
CrocSwap supports JIT thresholds from 0 to 255 seconds. A value of 0 seconds places no restrictions on JIT liquidity, as it allows LP positions to be minted and burned within a single block. Any value higher enforces a minimum resting time, and therefore atomic JIT within a single block is not possible. An upper limit of 255 seconds is enforced, because it prevents the protocol authority from arbitrarily locking LP owners out of redeeming their underlying position.
Protocol Gating
It's assumed that CrocSwap governance will dynamically adjust JIT thresholds depending on market quality. JIT liquidity may be allowed to the extent it constitutes a small component of the overall market and generally improves execution quality. On the other hand, if it starts to lead to a degradation in market quality, it can be restricted by protocol governance in any or every pool.
Since protocol governance can atomically revise JIT thresholds, another possibility is for permissioned JIT. The protocol authority can deploy a sidecar that atomically and temporarily lowers the JIT threshold to zero, but only for whitelisted participants. This may be used by the protocol governance to assure market quality by requiring whitelisted JIT traders to maintain long-term commitments in terms of execution quality and low toxicity. We expect the ability to gate JIT access to create demand for the CrocSwap governance tokens.
Pools Types
CrocSwap supports an effectively unlimited number of pool types, mapped individually to an arbitrary pool index. New pool types are created as a pool template by the protocol authority. Any user can initialize a pool from a pool template for any arbitrary pair of assets. Certain parameters for a pool type can be retroactively modified by the protocol authority, even on pools that were already initialized.
Parameters that define a pool type and can be changed retroactively are:
- Fee rate - The swap fee represented as hundredths of a basis point (0.0001%).
- Protocol take - The fraction of the swap fee that accumulates to the protocol. 0% if zero, otherwise 1/n.
- Tick size - The minimum granularity in number of price ticks (excluding grid improvement —see above).
Parameters that are part of pool type and cannot be changed:
- Permission oracle - The address of an external smart contract that arbitrates who can use the pool and when (if set to zero, the pool is permissionless).
Pool Initialization
With most DEX protocols the creation of a new liquidity pool requires creating a new smart contract to specifically support that pool. In CrocSwap all liquidity pools across all pairs live on a single smart contract. That means that initializing a new pool is a significantly lighter weight operation that costs an order of magnitude less gas than the traditional approach of deploying a new smart contract instance. All CrocSwap needs to do to initialize the curve is set a handful of variables to define a new pool and its associated pool.
The caller of the pool initialize function arbitrarily sets the initial price of the pool. Ultimately this is a relatively meaningless power. If the pool is mis-priced, any market participant is incentivized to swap against and earn arbitrage profits until the price is correctly set. Even for orders of magnitude mis-pricings, the gas cost of a large impact swap is capped in terms of gas.
CrocSwap sets a protocol-wide initial lock liquidity commitment. This threshold is initially set to zero and can be revised by the protocol authority at any time. It represents a minimum amount of ambient liquidity that the pool initializer must permanently commit to the pool. The pool initializer must permanently commit collateral on both sides of the pair to support this ambient liquidity, at the price they choose to initialize the pool at. The value is set to an economically meaningless amount for any common token pair. (E.g. 10e6 liquidity corresponds to less than $0.0001 of collateral for ETH/USDC.)
That improves the ecosystem and prevents spuriously initialized or mis-priced pools. First, because ambient liquidity always requires collateral from both sides, it means that the pool initialize must have access to both tokens in the pool. This prevents pools being initialized on tokens that don't exist or which don't have any unlocked supply. Second it penalizes manipulators from setting very extreme inflated prices on worthless tokens against hard currency cross pairs.
Permissioned Pools
CrocSwap supports the creation of permissioned pools. Any external user of a permissioned pool is gated by a permit oracle. A permit oracle is any arbitrary external smart contract that implements the ICrocSwapPermitOracle
interface. (A pool without an attached permit oracle is a permissionless pool).
A permit oracle may or may not be under the control of CrocSwap governance. However CrocSwap governance must always approve the use of a permit oracle, by initializing a pool type with an attached permit oracle. Once created, the permit oracle associated with a pool can never be changed. (Including that a permissionless pool can never become permissioned.)
The permit oracle interface directly accepts the following data at call time:
- User address
- Base and quote token address
- Type of action being executed (swap, mint, burn or compound)
Each permit oracle call must return a simple true/false if the action is permitted. However the depth of information being considered can easily be extended by gating all calls to a permit pool inside the oracle contract itself.
The most straightforward application for permissioned pools is whitelisting. LP and swap users could share the same whitelist, or be whitelisted by separate criteria. Whitelisting could be token specific. Whitelisting might only be done for LPs, but not swappers or vice versa.
Another application is sponsored pools. The ability to stake liquidity in a pool might be open to all users, but the ability to swap against the pool restricted to a single user or protocol. The swap could expose liquidity from CrocSwap at a centralized exchange or another chain. That's not possible with permissionless pools, because another swapper could access the liquidity before the sponsor.
We anticipate permissioned pools to be a highly useful new primitive across a range of possible applications in the DEX space. The ultimate ability to approve new types of permissioned pools should be a source of eventual demand for CrocSwap governance tokens.
Tradable Actions
The fundamental purpose of CrocSwap is to execute tradable actions across its pool. Tradable actions are any calls made by users that interact with the liquidity in a single pool. Tradable actions are built from core primitives, which include the following:
- Swap —Convert one asset into another within a single pool.
- Mint Range — Create and assign an LP position within a user specified price range on a single pool. If called on a pre-existing order, increases the total liquidity.
- Burn Range — Remove all or part of the liquidity associated with with a range order. Will also return the accumulated ambient liquidity rewards earned by that concentrated liquidity.
- Mint Ambient — Create and assign a classic constant-product AMM liquidity position that remains active at every possible price.
- Burn Ambient — Remove all or part of the liquidity associated with an ambient order. Will also return the accumulated liquidity rewards earned by the position.
CrocSwap exposes two parallel facilities for top-level calls on the contract to compose core primitives. The first is long-form order directives. This is a custom space-efficient byte-encoded format that allows for callers to encode any arbitrary sequence of core primitives across one of multiple pool types chained across a user-defined chain of one or more overlapping pairs.
The advantage of long-form order directives is they allow for arbitrarily complex actions to be executed with a direct call to the CrocSwap contract, avoiding the need for an intermediary order router or peripheral smart contracts. Long-form orders also net out the settlement of any token collateral across all the tradable actions across pairs.
The other facility for tradable actions are simple directives. These are CrocSwap methods that are restricted to performing a single primitive (swap, mint or burn) on a single pool per call. Because of their lower complexity the gas cost is significantly better. Many use cases only require a single primitive within a transaction. And again these cases avoid the need for any additional router or peripheral smart contracts. Of course, simple directive calls can always be stacked in a single transaction by custom outside smart contracts (see External Routers section).
Rolling Gap Fill
For long-form order directives, CrocSwap execution engine supports the ability to use rolling gap fill quantities. This allows the user to specify swaps, mints or burns with dynamic sizing that's calculated at evaluation time to counter-balance the token flow previously accumulated when executing the directive.
One example is multi-pair swaps. For example a user may start with a fixed amount of USDC, convert that into ETH in the USDC/ETH pool, then want to convert whatever was the output from that result into WBTC on the WBTC/ETH pool. In the WBTC/ETH swap directive, the user would encode the swap quantity using a dynamic rolling gap fill. That would forward the full amount outputted from the USDC/ETH swap into the WBTC/ETH, even if higher or lower than expected by the user.
Another example is a user that wants to mint liquidity on the USDC/ETH pair but only has ETH to commit as liquidity collateral. The order directive can call a mint primitive, which creates a USDC debit. They can then add a swap directive with rolling gap fill quantity, which will dynamically convert as much ETH on the pool as needed to meet the USDC required. Similarly, they could start with a fixed amount of ETH to convert to USDC, then use rolling gap fill on the mint primitive. That would dynamically scale the liquidity to however much can be supported by the newly converted USDC.
Trade and Collateral Settlement
Because CrocSwap consolidates the full state of the DEX into a single smart contract address, it allows for co-mingling asset collateral across pools. For example, a user is executing a multi-hop swap that crosses multiple pairs, there is no token transfer event necessary for the intermediate token. While the user may have a debit of the token on the first pair, they have an offsetting credit on the second pair, and therefore the flow nets out to zero.
Another example is an existing LP who wants to reposition their range order or move it to a different pool type within the same pair. On Uniswap V3 this requires returning the full amount of collateral back to the user on the burn, then the user sending the full collateral back to the protocol on the subsequent mint. In CrocSwap if there's no net change to collateral balance, the LP can avoid any token transfer events. This is highly useful for high-frequency LPs who repeatedly re-repositions their LP positions or toggle liquidity between fee tiers.
Avoiding unnecessary token or Ether transfers adds the following advantage over traditional DEX settlement approaches:
- Reduces gas fees by avoiding unnecessary external token transfer calls.
- Allows users to sell, mint liquidity or arbitrage tokens that they don't currently own as long as the net compound transaction is zero or positive.
- Eliminates the need for users to make a costly approve token call for any intermediate tokens used in a multi-hop swap.
- Eliminates taxable events. Every time a token transfer occurs, the user has a taxable event on the token at fair market value. Not only does this create administrative cost, but in the example of intermediate tokens it may create realized gains or convert long-term to short-term gains in a token that they're not even trying to trade.
Native Currency Collateral
CrocSwap supports using raw Ether (or the equivalent native currency on any EVM chain) instead of an ERC20 token in an asset pair. Most DEXs are restricted to ERC20 tokens and therefore must use the Wrapped Ether tokens. This is inefficient because ERC20 token transfers require more gas than native Ether transfers. This change alone enables a significant reduction in gas ees for swaps involving Ether (the bulk of all DEX trades today.)
Surplus collateral
Besides holding collateral associated with LP positions, CrocSwap allows users to deposit collateral (in the form of tokens or native Ether) directly at the exchange. Any collateral debits created by a tradable event can be paid directly from this surplus collateral balance without requiring an external transfer. The surplus collateral can either be deposited directly or specified as the destination for the net output of any tradable action.
This substantially reduces gas costs, particularly for active traders. Imagine a day trader who's continuously buying and selling positions across many pairs. She might start the day by depositing USDC as surplus collateral at the exchange. Based on the market activity she may execute tens or hundreds of trades.
There's no reason for her to directly receive the token output from all of these swaps and pay the associated gas fees. Instead each swap would output to her CrocSwap surplus collateral balance. When finished, she'd net the entire collateral flow into a single surplus withdraw, saving substantially on aggregate gas costs.
Oracle
CrocSwap does not provide any sort of historical price oracle. This tradeoff exists because the gas cost imposed by oracle storage is paid by swap users, while their benefits accrue to off-protocol systems that pay much lower gas fees to read the oracle. Given the high-cost gas environment of Ethereum today, we decided to not include a native oracle inside the protocol. However, the internal state inside any CrocSwap pull is easily visible. The single-contract DEX architecture makes it relatively simple and low-cost to batch oracle snapshots across many different CrocSwap pools.
External Routers
The CrocSwap smart contract exposes a fully powered interface that's sufficient capable of executing arbitrarily complex trading actions across multiple pools or pairs directly in the core protocol. For that reason, it doesn't need separate smart contracts in a "periphery" to serve as order routers.
This has two advantages. One it reduces gas fees, as the EVM imposes a gas cost for touching multiple smart contracts. Two it equalizes the disparity between regular users and sophisticated power users who directly interface with the individual pair contracts in the core. In CrocSwap, regular and power users share a common interface.
However, CrocSwap provides a flexible, low-cost facility to expose custom behavior to external routers or smart contract aggregators. (Typical users who directly interface with the CrocSwap smart contract from their externally owned address can ignore this section.) These settings determine how CrocSwap assigns LP and token collateral custody when called by an external router.
Rather than specifying behavior through gas consuming call parameters, CrocSwap interacts with routers differently based on their smart contract address. That means that any external smart contract will always enforce consistent custodial behavior that users can verify simply by checking the address.
The "default" custodial options are below:
- Token settlement credits are paid to
msg.sender
— therefore the user should trust that the router smart contract correctly pays them what they're owed.
- Token debits are charged to
msg.sender
— therefore router developers must externally collect from the user.
- LP Positions are indexed by
msg.sender
— therefore the calling router smart contract "owns" the liquidity position and user should trust the router to manager their liquidity.
Routers can override this behavior at the smart contract level with the following options:
- Credits are paid to
tx.origin
— therefore the user doesn't have to trust the router and potentially avoids an unnecessary token transfer
- Debits are charged to
tx.origin
— therefore the router can avoid an unnecessary external collection. (For security reasons, each user address must independently approve the specific router address to use this option.)
- Positions are indexed by
tx.origin
— therefore users directly own the LP and aren't dependent on the router after the mint. (For security reasons, any burn operation using this feature requires the user to have previously approved the router address.)
- Positions are indexed by a joint hash of
tx.origin
andmsg.sender
— this is useful because it means that routers no longer have to internally track their users' individual positions. Also, this enables staking contracts to track the creation time of each LP separately.
Magic Addresses
To support a variety of custodial options, CrocSwap implements different functionality depending on the address of the msg.sender
value (but only when different than tx.origin— therefore externally owned accounts behave the same regardless of address.) The reason is to avoid the extra gas overhead of passing explicit arguments for different custody options. External router developers can use the CREATE2 opcode to select an address to enable the specific CrocSwap functionality they want.
Default router functionality is enabled unless the last byte of the router's address equals the magic code of 0xCC
. External router developers should always check the address of their deployed contract, but this assures that if they don't that there's a 99.6% chance the router contract has default functionality.
If the magic address is enabled then the above functionality is toggled on or off depending on the next to last bytes preceding the magic code at the end of the address. Each of the above functionality options is toggled by bit flags in this byte. External router developers can use CREATE2 to mine an address corresponding with the functionality they want.
Smart Contract Design
As mentioned above, the CrocSwap protocol contains its entire state in a single smart contract address. The contract at this address provides a single interface point for external users. Because of Ethereum's 24 kilobyte limit on contract size, it is impossible to fit the above functionality into the code of a single address.
To counter that CrocSwap uses sidecar contracts to contain additional code. These sidecar contracts should never be called directly. They only exist to store code— not state, and are only accessed through internal delegate call operations that operate on the EVM state of the main contract.
Hot and Cold Call Paths
CrocSwap uses the concept of a hot path, containing the most gas critical code because of a combination of its frequency of use and the gas cost sensitivity of its users.
In the context of the smart contract size limit, this boils down to what code paths live directly in the primary CrocSwap contract versus what code is moved to sidecar proxy contracts. Because delegate calls across smart contract boundaries increase gas cost (both in terms of Ethereum's COLD_ACCOUNT_ACCESS_COST
as well as the overhead to encoding arguments and decoding return values) we want to minimize in gas critical contexts.
Based on the real world usage of DEXs, we choose to optimize for simple one-pool swap operations, first and foremost. These operations make up the sizable majority of all calls on current AMM protocols. Therefore the swap simple directive call is integrated directly into the primary CrocSwap smart contract without requiring a delegate call to a proxy contract.
Next in the gas optimization hierarchy is the warm path. This contains the second most gas critical functions: simple mint and burn directives. These are not called as frequently as swaps, but for users executing a simple operation, there's only a single action and therefore gas overhead cannot be amortized on multiple actions. In addition users of simple directives are foregoing more complex functionality, often because they're gas sensitive users.
The warm path is a single proxy contract. It's called through a single method on both the main contract and proxy contract. That minimizes contract size on both sides. The user also passes a pre-encoded byte array argument to the warm path method, which avoids the abi.decode
and abi.encode
in the primary contract, by letting the raw byte string cut through directly.
The long form order directive is called in the long path proxy contract. This supports the full array of functionality, because of the arbitrary nature of order directives. Because of this the full code doesn't fit entirely into a single proxy contract, and it uses a second proxy contract— micro paths. The micro paths sidecar contains small single calls for tradable core primitives. Each core primitive invoked in the long path when evaluating an order directive is a separate call to the micro paths sidecar. For this reason there's more gas costs than the previous layers.
Finally at the bottom of the gas hierarchy is the cold path. This proxy sidecar contains functionality that's rarely used, and therefore has very little need for gas optimizations. Primarily this means new pool initialization and protocol governance actions. These calls occur orders of magnitude less frequently than the core tradable actions and therefore are the least gas critical.
Internal Representations
Prices
Each pool's liquidity curve has an independent price. (In practice prices between pair triangles and pools within the same pair are linked by market arbitrage. But the CrocSwap protocol does not rely on any market arbitrage assumptions.)
A curve's price reflects the exchange rate between the quote and base asset in the pair. Higher price implies less quote assets will be swapped for a fixed base asset. The CrocSwap contract internally represents prices as the square root of the virtual reserve ratio:
This square root price ratio is represented as a Q64.64 fixed-point number stored as a uint128
in the smart contract code. The ratio represents the price between the wei of the tokens in the pair, and does not take into account the decimal deflator on the ERC20 token contract (or the 10^-18 deflator for native Ether).
Q64.64 precision effectively supports all major token pairs. Practically speaking, the most expensive tokens on a "per-wei" basis are the wrapped BTC tokens which have a market price on the order of $100,000 and a decimal deflator of 8 digits. The cheapest actively traded tokens have a price of $0.00001 and an 18 digit deflator. The square root of the price ratio between these respective extremes would be bounded at (or if pair is flipped).
Therefore Q64.64 is more than adequate to represent any reasonable pair's market price. And if not, it's trivially easy to create a wrapped token with a different decimal deflator that behaves identical to the original token.
The other consideration when storing prices is precision. Any economically meaningful change in a pair's market exchange rate must be reflected inside the encoded price variable. Practically speaking, there are two types of pairs: "pegged" and "floating". A pegged pair will have a price ratio on the order of 1.0, and therefore in a Q64.64 representation, precision can reflect price changes at a size of .
Floating pair requires much less precision, because end users aren't sensitive to small fluctuations. Typical token price moves are at least 0.1% for any reasonable holding period. An acceptable level of precision is representing price changes at the granularity of 0.001% (in practice this matches the tick size found at most centralized exchanges). To achieve that we need a minimum of 16 bits of precision at the fixed point representation.
Therefore CrocSwap enforces a minimum price value of (or for the square root ratio representation that's stored on chain). Practically speaking, that's four orders of magnitude smaller any price we'd ever expect to see for an economically meaningful pair.
To leave a buffer for safe arithmetic within the EVM's native 256-bit integers, the price is bounded on the upside at .
Price Tick
CrocSwap uses price ticks to represent the position of range orders and the boundaries of concentrated liquidity bump points. Ticks are represented as an int24
value in the CrocSwap smart contract code. A tick's price is mapped by the following formula:
Since prices are actually processed in terms of their square root, more practical is the mapping to the root price:
This comfortably encompasses the full range of possible representations from the Q64.64 price root representation.
Liquidity
In CrocSwap liquidity reflects the amount of virtual reserves in the pool (and therefore the magnitude of the market impact of a fixed size swap). It's represented as the square root of the product of the virtual reserves:
Combined with the square root representation of price this allows for safe, cheap and simple arithmetic to calculate flows related to tradable events. For example the amount of base token that we can swap until we move the price by a fixed amount:
The curve object tracks concentrated liquidity and ambient liquidity separately. Total liquidity is the sum of these two sub-aggregates:
Liquidity of both types is stored and represented as a uint128
in the smart contract code. Practically speaking no economically meaningful tokens have over a quintillion supply (at an 18 decimal deflator). Therefore is an adequate upper limit on the representation for any meaningful token.
Fee Accumulation
To allocate LPs their pro-rate share of a pool's earned liquidity fees it's necessary to keep a running tally of the cumulative growth in per-unit rewards since the inception of the pool. This must be done separately for concentrated and ambient liquidity. Both ambient and concentrated liquidity accumulate rewards in the form of compounded ambient liquidity. How we represent this for each side is different.

Rather than actually storing aggregate ambient liquidity itself we store the total quantity of ambient seeds active in the curve. One ambient liquidity seed represents the equivalent of one unit of ambient liquidity had it been added to the pool at inception. Because ambient liquidity compounds over time, the "exchange rate" between seed and liquidity deflates over time according to the cumulative growth rate:
(where A is the ambient seed value, L is the ambient liquidity and g is the cumulative per-unit growth rate since pool inception.)
Since ambient liquidity is always in range the cumulative growth rate increments by the earned fees divided by the total liquidity in the pool. First define the per-unit liquidity rewards associated with a single swap event at time T:
(Collected fees are converted to liquidity space by swapping them into the pool at the time of the swap.)
LPs with ambient positions receive back collateral equivalent to the deflated value of their seeds over the period they staked liquidity:
Concentrated LP positions also earn rewards in the form of ambient liquidity. Therefore concentrated rewards are not represented as cumulative compound growth rates, but the per—unit cumulative sum of ambient seed rewards received over time. Let's define the cumulative ambient seeds earned by a single unit of concentrated liquidity had it stayed in range since pool inception:
Because concentrated rewards are denominated in ambient seeds, a concentrated LP's accumulated rewards continue to compound as the ambient seed deflator grows over time with cumulative liquidity fees collected.
Both ambient and concentrated cumulative growth rates are stored as Q16.48 fixed point numbers stored in a uint64
. This allows for incremental growth as small as . Assuming a 0.05% pool fee rate, this is enough precision to support collecting fees on as little as $1 of swapped value on a pool with $100 billion of liquidity. It's unlikely that precision smaller than this level will ever be economically meaningful. Smaller swaps on larger liquidity pools will still function but simply not increment the cumulative growth rate.
This representation also sets an upper-cap on cumulative growth at . That would represent a cumulative growth on an ambient LP position of 6,500,000%. Even assuming liquidity fees generate APYs of 50% it would take 27 years for a pool to reach "max" cumulative growth. In which case the pool will still continue to function, but simply stop accumulating rewards for its LP. Because of CrocSwap's flexible pool type system, a new pool type with a "fresh" cumulative growth can be created at this point.
Token Quantities
All token quantities whether stored or in the smart contract interface are represented as either uint128
or int128
. For this reason, an effective upper bound of is set on the number of tokens that can be processed at any one time. Practically speaking, no economically meaningful token has a supply above this size. And if it does, a wrapped token with a larger digit deflator can be used instead.
Tick Bump Bitmap
In order to safely execute swaps over a locally stable AMM curve we need a gas efficient approach to randomly access which price ticks have active concentrated liquidity bumps. This is represented as a nested series of bitmaps stored on uint256
slots.
At the lower layer a single uint256
"terminus" bitmap will represent which ticks in a 256-tick range have active bumps. Few swaps will have to read from outside the terminus bitmap associated with the current price. A single terminus bitmap represents a price range of 2.6%. Practically speaking, only a small fraction of swaps move the price by more than 1% at a time.
In the case of a swap that "spills" outside the local terminus bitmap, it will read the "mezzanine" bitmap. Each bit in the mezzanine bitmap represents the full 256-tick range of a single terminus range. A mezzanine bit is flipped on if any bits in that terminus range have an active liquidity bump. This allows us to efficiently find the next bump for large price impact swaps on sparsely populated concentrated liquidity curves.
Each mezzanine bitmap in total represents a 65,536 tick range. Therefore we'd only expect a swap to spill over the mezzanine on moves over 1,000%. Practically speaking, swaps of this magnitudes are extremely rare on any economically meaningful pool.
Finally, in the case that a swap does "spill" over the mezzanine, mezzanine bitmaps are tested in linear order in the direction of the swap. Because tick is an int24
there are at most 256 mezzanine bitmaps, and even in the worse case the gas cost of seeking the next concentrated bump point (if any) is bounded at 256 SLOAD operations.
Tick Indexed State
Each pool maps tick index to information related to aggregated concentrated liquidity at that level. Using the tick bitmap data, the protocol knows which bump points will be cross by a swap (if any), and can retrieve update the tick indexed data as necessary. On mint or burn operations the user directly specifies the tick index of the range order boundaries and therefore the protocol knows which indexed state to update.
Each tick indexed position holds the following data:
- Bid lots (
uint96
) - Represents the aggregate amount of concentrated liquidity (in multiples of 1024) from range orders with a lower boundary at this tick.
- Ask lots (
uint96
) - Represents the aggregate amount of concentrated liquidity (in multiples of 1024) from range orders with an upper boundary at this tick.
- Rewards odometer (
uint64
) - Checkpoints the cumulative concentrated growth rewards growth since the last time the curve price crossed the tick.
The reason liquidity is stored in terms of "lots" (multiples of 1024) is so the entire tick indexed state can fit in a single 256-bit slot. This reduces gas costs associated with crossing a bump point or minting a concentrated liquidity position. Practically speaking, less than 1024 wei of a token is almost always economically meaningless, so the ability to mint concentrated liquidity at less than 1024 units of granularity is not useful. In the worse case the user can mint ambient liquidity, which is not subject to the same granularity restrictions.
With the lots representation, the tick-indexed state is restricted from aggregating more than at a single tick. There's very few tokens where supply is high enough that this restriction is economically meaningful at a single tick.
The reward odometer for a given tick is initialized as follows:
if
Otherwise
(where is the cumulative global concentrated liquidity growth rate defined in the Fee Accumulation section above)
Every time a tick is crossed the odometer is updated based on the following iteration:
Effectively the odometer tracks the cumulative growth rate that occurred on the opposite side of the tick from the price (with all the growth prior to initialization time arbitrarily treated as occuring above the tick).
if
Otherwise
Define the post-initialization below tick growth rate as
Because the offset is static after initialization time:
For a lower tick, L and an upper tick U the rate of cumulative concentrated growth that has occured within range is
Finally the concentrated liquidity rewards growth within a tick range, within a time range can be precisely calculated, because the offset is static and cancels out from the start and end time term:
Being able to calculate the cumulative growth rate in a given range over a given time will be important is determining an individual range order's accumulated rewards over its lifetime.
Position Indexed State
CrocSwap supports to type of liquidity positions: ambient orders and concentrated orders. On a per-pool basis ambient LP positions are indexed by the owner's address and nothing else. Two values are stored in an ambient position:
- Ambient Seeds (
uint128
) - The total number of ambient liquidity seeds owned by the position. No reward counter is needed because ambient seeds deflate over time as the pool accumulates rewards
- Block Time (
uint32
) - The Unix timestamp of the block that the position was created (or last incremented). Not used by CrocSwap for any purposes but can be used externally by staking reward contracts.
LPs can increase the size of their ambient LPs. To do so they specify the number of ambient seeds to add, and they commit liquidity collateral based on the cumulative ambient seed growth rate at the time of the incremental mint. Increasing liquidity will always reset the block time on the position. (Otherwise a staking program may erroneously credit a user who staked a tiny quantity for a long time until the last minute when they increased their liquidity.)
Ambient LPs can burn part or all of their ambient position by specifying the number of ambient seeds to burn. CrocSwap will return liquidity collateral based on the cumulative ambient seed growth rate at the time of the burn. Burning will not reset the block time on the position.
Concentrated LPs mint positions as range orders which are bound to fixed price ticks. Within a pool these positions are indexed by the address of the position's owner, the lower tick index, and the upper tick index. The position stores the following variables:
- Liquidity (
uint128
) - The amount of concentrated liquidity with the position. (As rewards accumulate, ambient liquidity will become associated with the position, but that isn't reflected by this metric.)
- Rewards Odometer (
uint64
) - The cumulative growth rate of concentrated rewards within the tick range, adjusted with an arbitrary offset ( from the previous section).
- Block time (
uint32
) - The Unix timestamp of the block that the position was created.
Like ambient liquidity positions, block time is always reset when liquidity on a position is increased, but never when liquidity is decreased.
The rewards odometer is used to calculate the accumulated ambient liquidity seed rewards associated with the position. When the position is burned the most recent value will be compared to the odometer snapshot in the position, and that determines the growth rate for the position:
That value is multiplied by the concentrated liquidity in the position to determine the number of ambient seeds owed to the the position owner:
A position can be partially burned, in which case the rewards odometer remains the same, the concentrated liquidity stored by the position is decremented, and the rewards are paid out scaled to the amount of concentrated liquidity being burned (not the entire liquidity in the position):
Incremental liquidity can also be minted to an already existing concentrated LP. To do so the concentrated liquidity is incremented with the additional liquidity. The rewards odometer is reset at the blended rate of the previous rewards and the recent snapshot:
This assures that the blended position maintains the exact rewards that would exist as if the two incremental mints were stored as separate positions.
Co-Mingled Collateral Stability
Pools in CrocSwap do not individually track their real reserves. Therefore CrocSwap has to protect collateral stability by assuring that no sequence of tradable actions ever results in the protocol being under-collateralized.
We define the required real reserves of an individual pool as the total base or quote collateral required to pay back the collateral for every LP position in the pool at the curve's current price.
We define the required real reserves of the protocol as the total required real reserves of all individual pools in the protocol. Pools are defined over arbitrary token pairs, the protocol has a required real reserve for every token with an active pair:
The protocol is collateral stable (for any give token) if and only the DEX smart contract holds an equal or larger balance that the protocol required reserves.
Any tradable action (T) in CrocSwap results in a change in the state of a single pool's curve:
Because each tradable action only affects a single pool the change in protocol reserves is equal to the change in the pool specific reserves
Therefore if the protocol starts in a state of collateral stability, then any tradable action maintains collateral stability if and only if the token flow to the DEX is equal or more favorable:
By default all pools are initialized with empty reserves:
Since token balances on any contract initialize at zero, the protocol is initialized in a state of collateral stability. By making sure that all possible legal tradable actions are matched by an equal or more favorable DEX token flow, then the protocol is guaranteed to always be in a state of collateral stability.
Hence CrocSwap is able to co-mingle collateral in a single undifferentiated supply. The reserves of individual pools are not separately tracked, because collateral stability is maintained on a per-delta basis across the DEX protocol.
https://crocswap-assets-public.s3.us-east-2.amazonaws.com/CrocSwap_Whitepaper.pdf