There are four functions for settling the order (described
here), which operate similarly but differ in passed off-chain signatures and additional arguments passed to a taker.
fillOrder() and
fillOrderArgs() check (v, r, s) off-chain signature set by the user.
fillOrder() passes
empty args (extension) to the taker, and
fillOrderArgs() sets additional arguments.
fillContractOrder() and
fillContractOrderArgs() work similarly with taker args (extension) but check
bytes calldata signature, a signature made by a contract according to
EIP-1271 when orders are created by a smart contract.
These two groups of functions then go to
_fillOrder() or to
_fillOrderContract(), which perform "first-level" checks,
checking for remaining amounts (if the order allows for partial fill) and attempting to
apply maker permits (for user-signed orders).
Both groups of functions conclude with the call to the main
_fill() function - our main target.
First, a series of different
validations occur. We start with a check of the
validity of the
extension by using
isValidExtension(), a function that checks if extension contents are hashed to the order's
salt parameter, protecting the maker from possible taker's modification of extension parameters.
Next is the
pack of checks related to order sender and expiration parameters.
Following this is the
predicate check - an external call defined by the maker that determines if the order is allowed to be fulfilled. It uses the
_staticCallForUint() function to perform a static(!) call to the target, which must return "1" to continue processing the order. This functionality allows makers to add almost any "limit" conditions to their orders and configure limit order conditions as they see fit.
The next part is processing maker and taker amounts. Similar to DEXes, there are
two branches: one where the taker uses an "exact making amount" (analogous to
swapExactIn() in Uniswap) and another "exact taking amount" (analogous to
swapExactOut()). Both branches calculate target amounts for maker and taker (
here and
here) and check thresholds for
TakingAmountTooHigh or
MakingAmountTooHigh. The choice of which branch to pursue in TakerTraits is the taker's responsibility, allowing them to choose how to fill the order.
Finally, making/taking amounts are
checked to ensure they are non-zero and that they correspond to the full order amount if the order isn't partially fillable.
Next,
invalidation status is checked using the
BitInvalidator. This feature allows a maker to set an
invalidation bitmap for the given maker's address, enabling them to invalidate other orders made by the same maker. For instance, one filled order from a pack can invalidate all others, allowing the implementation of strategies like "fill one order from a pack."
BitInvalidator can use
nonce identifiers to group orders or employ the maker's
epoch concept, permitting trading services to publish multiple orders in one epoch, then move the epoch, invalidating all "old" orders, and continuing in the next epoch. Further information about this type of invalidation can be found
here.
Next, invalidation of an order based on
remaining maker's amount occurs, which is straightforward.
Then comes
pre-interaction (if set in
MakerTraits). Here, data related to this call is loaded from the order's
extension, and the
listener address is called (if provided) or the maker's address otherwise. We will examine different extension interactions, but for this part, we can review the
preInteraction() function in
extensions/ApprovalPreInteraction.sol, which simply provides the necessary
approve.
Next, the part
transferring assets from maker to taker. This involves potentially wrapping/unwrapping WETH if needed, and using two methods for transfers:
- Based on Permit2 (read more here)
- Based on transferFrom, but with an optional suffix that can be added to transferFrom parameters (if set in the extension), implemented here. This suffix is used for non-standard transfers, such as ERC-721 transfers, which require adding the tokenId to the parameters.
Then
goes TakerInteraction, which allows any actions that the taker wishes to add to the settlement after receiving the assets. Its interface is declared
here.
Following that,
transferring assets from taker to maker (or the receiver set by the maker) occurs. This includes handling WETH wrap/unwrap operations and using two types of transfer: based on
Permit2 and
transferFrom with suffix (similar to previous transfers from maker to taker).
Finally,
post-interaction follows the order filling, working similarly to pre-interaction described above.
Another form of "filling" an order is its cancellation, implemented
here. In the case of a
BitInvalidator, it invalidates the range of bits, rendering all orders with the same epoch or nonce invalidated. In other
cases, it marks the order as fully filled, invalidating it.
The order is filled, all conditions are checked, and tokens are transferred. Now, it's time to look at the process of creating orders and the protocols that use LOP.