Skip to main content

Restricted Swap

Overview

RestrictedSwap provides a secure means of swapping tokens between known holders of the primary Restricted token and any payment ERC-20 tokens (ie USDC, DAI, etc).

Note: Restricted Swap is not intended between two Restricted tokens. Rather, it is intended as a purchase of Restricted tokens using ERC-20 tokens (AKA payment token or quote token). This is enforced in the Restricted Swap contract itself, by preventing ERC-1404 compatible tokens (checked via ERC-165 interface support) from being used as payment. However, other types of Restricted tokens may not explicitly support the ERC-1404 interface and thus cannot be checked programmatically; therefore, this functionality should be used with caution and payment token types should always be verified first.

Supported Quote Tokens

The swap contract guarantees the recipient's side of every quote-token transfer, and only that side. Each settlement path measures the recipient's balance delta around the transfer and reverts RestrictedSwap_InconsistentQuoteTokenAmount unless it equals the quoted amount exactly, so a seller is always paid to the wei.

This rules out the common fee-on-transfer shape, where the fee is carved out of the transferred value and the recipient receives less than was sent (USDT's basisPointsRate when enabled, reflection tokens). Rebasing tokens are likewise unusable, since their balances move for reasons other than the transfer.

The payer's debit is deliberately not measured. A quote token that surcharges the sender on top of the transferred value — debiting amount + fee while crediting the recipient amount — settles successfully, and the payer's real cost is whatever that token does. Nothing in the contract depends on it: no funds are custodied, and every internal counter is denominated in the quoted amount, which is exactly what the recipient receives.

Integrator constraint. quoteTokenAmount is what the recipient receives, not necessarily what the payer is debited. Before presenting a swap's price to the paying side, check the quote token's transfer semantics — for a surcharging token the real price is higher than the ratio the contract advertises, and neither the SwapConfigured event nor the view helpers will show the difference. Verifying the payment token is part of listing an order, as it is for the ERC-1404 check described above.

Usage

How It Works

As a prerequisite, users of the swap functionality are assumed to have passed any necessary off-chain AML/KYC and deemed valid holders. Contracts are also assumed to have been configured properly with admin-granted roles and transfer restrictions in place.

Configuring a Purchase of Restricted Token

As a holder (Buyer) of an ERC-20 payment token (ie USDC, DAI, etc), one can configure a purchase order for a specified amount of Restricted token with a known party (Seller). Here is an example involving USDC:

  1. Buyer must know in advance specifically how much Restricted token they'd like to receive, how much USDC they are willing to pay, and the Seller on the other end.
  2. Buyer must approve the Swap contract itself to handle their USDC in the purchase amount required.
  3. Buyer then configures an open purchase order by calling configureBuy with the following parameters. A Swap ID is emitted in an event.
    • amount of Restricted token desired
    • address of Seller
    • address of payment token contract
    • amount of payment token (USDC) willing to swap
    • deadline for swap expiration (required; must be in the future and within the contract's maxSwapLifetime)
    • minimum fill amount (must be 0 for this closed order)
  4. Seller must approve the Swap contract itself to handle their Restricted token in the sell amount required by the configured swap.
  5. Seller can complete this order by calling completeSwapWithRestrictedToken with the emitted Swap ID (passing 0 for the optional parent sell offer, since this bid does not draw from one).

When transfer restrictions are validated between Buyer and Seller, the swap is completed and assets are actually transferred between transacting parties.

Note: Role checks (e.g., pausing, admin-only operations) are resolved through the pre-deployed AccessControl contract referenced by the Restricted token and ancillary contracts.

Configuring a Sale of Restricted Token

As a holder of the Restricted token, one can also configure a sell order for a specified amount of desired payment ERC-20 token. It is extremely similar to the example above, except terms of the sale must be specified in advance. Here is an example involving USDC:

  1. Seller must know in advance specifically how much Restricted token they'd like to sell, how much USDC they require, and the Buyer on the other end.
  2. Seller must approve the Swap contract itself to handle their Restricted token in the sell amount desired.
  3. Seller then configures an open sell order by calling configureSell with the following parameters. A Swap ID is emitted in an event.
    • amount of Restricted token to sell
    • address of payment token contract
    • address of Buyer
    • amount of payment token (USDC) required
    • deadline for swap expiration (required; must be in the future and within the contract's maxSwapLifetime)
    • minimum fill amount (must be 0 for this closed order)
  4. Buyer must approve the Swap contract itself to handle their USDC in the amount specified by the configured swap.
  5. Buyer can complete this order by calling completeSwapWithQuoteToken with the emitted Swap ID.

When transfer restrictions are validated between Buyer and Seller, the swap is completed and assets are actually transferred between transacting parties.

Open Orders and Partial Fills

In addition to the closed (named-counterparty) swap flows above, RestrictedSwap supports open orders that any compliant counterparty can fill, with optional partial fills so a single listing can be consumed by multiple takers.

How It Works

  • Open Sell Order: call configureSell(...) with quoteTokenSender = address(0). The seller's restricted-token allowance is escrowed via requiredAllowance, and any compliant buyer can call takeOpenSell(swapNumber, fillAmount) (or takeOpenSellWithPermit(...) to bundle ERC-2612 approval) to settle.
  • Open Buy Order: symmetric — call configureBuy(...) with restrictedTokenSender = address(0). The buyer's quote-token allowance is escrowed and any compliant holder may call takeOpenBuy(...) / takeOpenBuyWithPermit(...).

Compliance (detectTransferRestriction) is deferred to take time for open orders because the counterparty is unknown at configure time. The check runs against the actual taker before the transfer.

Partial Fills

Takers may specify a fillAmount less than the remaining restricted-token amount. The pro-rata quote charge is computed as quoteTokenAmount * fillAmount / restrictedTokenAmount and must be a whole number — the contract reverts RestrictedSwap_AmountNotDivisible on any fillAmount that does not divide cleanly into the price ratio. This rules out rounding entirely: every fill pays its exact pro-rata share, no taker can ever overpay or underpay by even one wei, and no inventory can be siphoned via dust-fill griefing.

⚠️ Pick listing amounts with a large GCD

Because the pro-rata charge must be exact, the set of valid fillAmount values for a listing is fixed by the GCD of quoteTokenAmount and restrictedTokenAmount:

Valid fillAmount ∈ multiples of restrictedTokenAmount / gcd(quoteTokenAmount, restrictedTokenAmount), up to remainingRestrictedTokenAmount.

Listing (restricted / quote)GCDSmallest valid fillOutcome
1000 RLT for 100 USDC10010 RLTUp to 100 partial fills possible
1000 RLT for 250 USDC2504 RLTUp to 250 partial fills possible
1000 RLT for 7 USDC11000 RLT❗ Effectively no partial fills — only a full take works
997 RLT for 11 USDC (both prime)1997 RLT❗ Coprime — only a full take works

Coprime amounts silently force a single full-listing take. Sellers/buyers configuring open orders should:

  1. Round listing amounts to ratios with a large GCD (e.g. multiples of 10 / 100 / 1000 of the smaller side), or
  2. Accept that the listing is take-it-or-leave-it.

Off-chain orderbook UIs SHOULD validate the listing's GCD at configure time and surface a warning, since the constraint is not enforced on configureSell / configureBuy — only at fill time.

Cancellation refunds only the remaining unfilled amount, not the original; already-filled portions are not reversed.

Self-Fill Disallowed

A configurer cannot also be the counterparty to their own open order. takeOpenSell reverts with RestrictedSwap_SelfFillNotAllowed when _msgSender() == swap.restrictedTokenSender; takeOpenBuy reverts the same way when _msgSender() == swap.quoteTokenSender. This prevents wash fills, misleading OpenSwapFilled events, and using takeOpen* as a cheaper substitute for cancelSwap. Configurers who want to retire an unfilled listing must call cancelSwap.

Events

Each partial OR final fill emits:

OpenSwapFilled(swapNumber, filler, fillRestrictedAmount, fillQuoteAmount, remainingRestrictedAmount)

remainingRestrictedAmount == 0 indicates the order is now fully filled; the contract additionally emits a regular SwapComplete with the last filler in the counterparty slot.

Indexer Notes

  • An open order is recognizable by SwapConfigured carrying quoteTokenSender == address(0) (open sell) or restrictedTokenSender == address(0) (open buy).
  • A single open order may emit multiple OpenSwapFilled events before its final SwapComplete.
  • Off-chain orderbooks coordinating concurrent takers should serialize submissions per listing (SELECT FOR UPDATE) to avoid racing on the same on-chain remaining amount; lost races revert with RestrictedSwap_InvalidFillAmount or RestrictedSwap_AlreadyCompleted.

View Helpers

  • remainingRestrictedTokenAmount(swapNumber) — current unfilled restricted amount. On a Complete swap this is always 0; a Canceled swap keeps the unfilled tail as history, so pair it with swapStatus before treating a non-zero value as available inventory.
  • isOpenOrder(swapNumber) — true if the swap has a zero counterparty (open) and must be filled via takeOpenSell / takeOpenBuy.

Deadline Functionality

Every swap carries a mandatory deadline that automatically expires it after a specified timestamp. This prevents indefinitely active orders and bounds how long allowances stay reserved:

  • Required Deadline: deadline must be > 0 at configure time — the "no deadline" (0) sentinel is rejected with RestrictedSwap_DeadlineRequired.
  • Must Be In The Future: A deadline in the past reverts with RestrictedSwap_SwapExpired at configure time.
  • Bounded Horizon: deadline - block.timestamp must not exceed the contract's immutable maxSwapLifetime, or configuration reverts with RestrictedSwap_DeadlineExceedsMaxLifetime. maxSwapLifetime is fixed at deployment within [MIN_SWAP_LIFETIME_LIMIT, MAX_SWAP_LIFETIME_LIMIT].
  • Automatic Expiration: Once a swap's deadline passes, it becomes invalid and cannot be completed or filled.
  • Anyone Can Cancel Expired Swaps: Once a swap expires, anyone can cancel it to free up allowances.

Key Features:

  • Deadline is checked during swap completion - expired swaps cannot be completed
  • Expired swaps can be canceled by anyone, not just the original parties
  • The isSwapExpired() function allows checking if a swap has expired
  • All swap events include the deadline parameter for transparency

EIP-2612 Permit Helpers

Every ...WithPermit entry point bundles an EIP-2612 permit with the operation it performs, so a caller signs one ERC-2771 forward request instead of sending a separate approve first.

Sizing permitValue

permitValue is the value the caller signed. It is forwarded to permit verbatim and never recomputed on-chain, because an EIP-2612 signature binds the exact value: a contract that derived the value from live reservation state would invalidate every signature that drifted before it was relayed — including drift caused by an honest third party filling one of the caller's other open orders.

Size it off-chain as:

permitValue = requiredAllowance(caller, token) + <this operation's delta>

Rules that follow from permit overwriting (not incrementing) the allowance:

  • Cover existing reservations, not just this operation. A delta-only value would clobber the allowance backing the caller's other active orders, leaving them unfillable and out of sync with requiredAllowance.
  • The requirement is re-evaluated at execution time. permitValue only has to be at least the requirement when the transaction lands; the contract rechecks the resulting allowance and reverts RestrictedSwap_Insufficient{Quote,Restricted}TokenAllowanceAfterPermit if it is still short. Adding headroom is therefore the safe direction on an active order book.
  • Over-signing is allowed but not free. A value far above the requirement (e.g. type(uint256).max) forfeits the exact-sizing property the contract otherwise maintains, leaving the caller authorised beyond their remaining intent.
  • The permit is skipped when it is not needed. If the standing allowance already covers the requirement, permit is never called — so a value signed for exactly the requirement cannot trim headroom the caller set deliberately, and the signature's nonce is left unconsumed.
  • A replayed permit cannot grief the call. The permit call is wrapped in try/catch and followed by the allowance recheck, so a front-runner who submits the signature first (consuming the nonce) does not break the operation.

API Reference

Constructor

constructor(address restrictedLockupTokenAddress_, address trustedForwarder_, address accessControl_, uint256 maxSwapLifetime_)

Initializes the RestrictedSwap contract with configuration parameters.

Parameters:

  • restrictedLockupTokenAddress_ (address): The RestrictedLockupToken contract address
  • trustedForwarder_ (address): ERC-2771 trusted forwarder address for meta-transactions
  • accessControl_ (address): AccessControl contract address for role management
  • maxSwapLifetime_ (uint256): Maximum allowed distance between block.timestamp and a swap's deadline at configure time, fixed for the life of the contract

Requirements:

  • All addresses must be non-zero
  • Access control address must be valid
  • maxSwapLifetime_ must be within [MIN_SWAP_LIFETIME_LIMIT, MAX_SWAP_LIFETIME_LIMIT], else RestrictedSwap_InvalidMaxSwapLifetime

Administrative Functions

pause(bool isPaused_)

Pauses or unpauses the contract functionality.

Parameters:

  • isPaused_ (bool): True to pause, false to unpause

Requirements:

  • Caller must have CONTRACT_ADMIN_ROLE or TRANSFER_ADMIN_ROLE

Emits: Paused(account) or Unpaused(account)


Swap Configuration Functions

configureSell(uint256 restrictedTokenAmount, address quoteToken, address quoteTokenSender, uint256 quoteTokenAmount, uint256 deadline, uint256 minimumFillAmount)

Configures a sell order for restricted tokens in exchange for quote tokens.

Parameters:

  • restrictedTokenAmount (uint256): Amount of restricted tokens to sell
  • quoteToken (address): Address of the quote token (ERC-20) to receive
  • quoteTokenSender (address): Address that will provide the quote tokens, or address(0) to create an open sell order any compliant buyer can fill via takeOpenSell
  • quoteTokenAmount (uint256): Amount of quote tokens to receive
  • deadline (uint256): Unix timestamp deadline for swap expiration (required; see Deadline Functionality)
  • minimumFillAmount (uint256): Minimum restricted-token amount a taker must fill on an open order via takeOpenSell (unless taking the entire remaining amount); 0 means no minimum. Must be 0 for closed swaps.

Requirements:

  • Contract must not be paused
  • Quote token must not be zero address
  • Caller must have sufficient restricted token allowance for this contract
  • Quote token must not support ERC-1404 interface (to prevent restricted token swaps)
  • deadline must be > 0, not in the past, and within maxSwapLifetime
  • minimumFillAmount must be <= restrictedTokenAmount, and must be 0 when quoteTokenSender is a concrete address (closed swap)

Emits: SwapConfigured(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteToken, quoteTokenSender, quoteTokenAmount, deadline, minimumFillAmount)


configureBuy(uint256 restrictedTokenAmount, address restrictedTokenSender, address quoteToken, uint256 quoteTokenAmount, uint256 deadline, uint256 minimumFillAmount)

Configures a buy order for restricted tokens using quote tokens.

Parameters:

  • restrictedTokenAmount (uint256): Amount of restricted tokens to buy
  • restrictedTokenSender (address): Address that will provide the restricted tokens, or address(0) to create an open buy order any compliant holder can fill via takeOpenBuy
  • quoteToken (address): Address of the quote token (ERC-20) to pay with
  • quoteTokenAmount (uint256): Amount of quote tokens to pay
  • deadline (uint256): Unix timestamp deadline for swap expiration (required; see Deadline Functionality)
  • minimumFillAmount (uint256): Minimum restricted-token amount a taker must fill on an open order via takeOpenBuy (unless taking the entire remaining amount); 0 means no minimum. Must be 0 for closed swaps.

Requirements:

  • Contract must not be paused
  • Quote token must not be zero address
  • Caller must have sufficient quote token balance
  • Caller must have sufficient quote token allowance for this contract
  • Quote token must not support ERC-1404 interface
  • deadline must be > 0, not in the past, and within maxSwapLifetime
  • minimumFillAmount must be <= restrictedTokenAmount, and must be 0 when restrictedTokenSender is a concrete address (closed swap)

Emits: SwapConfigured(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteToken, quoteTokenSender, quoteTokenAmount, deadline, minimumFillAmount)


configureSellWithPermit(uint256 restrictedTokenAmount, address quoteToken, address quoteTokenSender, uint256 quoteTokenAmount, uint256 deadline, uint256 minimumFillAmount, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)

Same as configureSell, but approves the restricted-token allowance via an EIP-2612 permit signature so approve + configure land in a single (optionally relayer-driven) transaction.

Parameters:

  • All parameters of configureSell, plus:
  • permitValue (uint256): The EIP-2612 value the caller signed. Forwarded to permit verbatim and never recomputed on-chain — see Sizing permitValue
  • permitDeadline (uint256): EIP-2612 permit deadline (must be >= block.timestamp)
  • v, r, s: Components of the permit signature

Requirements:

  • Same as configureSell
  • The restricted token must implement EIP-2612
  • permitValue must cover requiredAllowance(caller, restrictedToken) + restrictedTokenAmount as of execution time (permit overwrites the allowance); the post-permit allowance is rechecked, reverting RestrictedSwap_InsufficientRestrictedTokenAllowanceAfterPermit if still short

Emits: SwapConfigured(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteToken, quoteTokenSender, quoteTokenAmount, deadline, minimumFillAmount)


configureBuyWithPermit(uint256 restrictedTokenAmount, address restrictedTokenSender, address quoteToken, uint256 quoteTokenAmount, uint256 deadline, uint256 minimumFillAmount, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)

Same as configureBuy, but approves the quote-token allowance via an EIP-2612 permit signature so approve + configure land in a single (optionally relayer-driven) transaction.

Parameters:

  • All parameters of configureBuy, plus:
  • permitValue (uint256): The EIP-2612 value the caller signed. Forwarded to permit verbatim and never recomputed on-chain — see Sizing permitValue
  • permitDeadline (uint256): EIP-2612 permit deadline (must be >= block.timestamp)
  • v, r, s: Components of the permit signature

Requirements:

  • Same as configureBuy
  • The quote token must implement EIP-2612
  • permitValue must cover requiredAllowance(caller, quoteToken) + quoteTokenAmount as of execution time (permit overwrites the allowance); the post-permit allowance is rechecked, reverting RestrictedSwap_InsufficientQuoteTokenAllowanceAfterPermit if still short

Emits: SwapConfigured(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteToken, quoteTokenSender, quoteTokenAmount, deadline, minimumFillAmount)


Swap Execution Functions

completeSwapWithQuoteToken(uint256 swapNumber_)

Completes a swap by providing quote tokens (used by quote token sender in sell-configured swaps).

Parameters:

  • swapNumber_ (uint256): The swap number to complete

Requirements:

  • Contract must not be paused
  • Swap must not be canceled or already completed
  • Caller must be the quote token sender for this swap
  • Swap status must be SellConfigured
  • Quote token sender must have sufficient quote token balance
  • Restricted token sender must have sufficient restricted token balance
  • Transfer restrictions must be satisfied between parties

Emits: SwapComplete(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteTokenSender, quoteToken, quoteTokenAmount, deadline)


completeSwapWithQuoteTokenPermit(uint256 swapNumber_, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)

Same as completeSwapWithQuoteToken, but approves the quote-token allowance via an EIP-2612 permit signature so approve + complete land in a single transaction.

Parameters:

  • swapNumber_ (uint256): The swap number to complete
  • permitValue (uint256): The EIP-2612 value the caller signed. Forwarded to permit verbatim and never recomputed on-chain — see Sizing permitValue
  • permitDeadline (uint256): EIP-2612 permit deadline (must be >= block.timestamp)
  • v, r, s: Components of the permit signature

Requirements:

  • Same as completeSwapWithQuoteToken
  • The quote token must implement EIP-2612
  • permitValue must cover requiredAllowance(caller, quoteToken) + quoteTokenAmount as of execution time (permit overwrites the allowance); the post-permit allowance is rechecked, reverting RestrictedSwap_InsufficientQuoteTokenAllowanceAfterPermit if still short

Emits: SwapComplete(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteTokenSender, quoteToken, quoteTokenAmount, deadline)


completeSwapWithRestrictedToken(uint256 swapNumber_, uint256 sellSwapNumber_)

Completes a closed buy order (or an accepted bid) by providing restricted tokens (used by the restricted token sender in buy-configured swaps).

Parameters:

  • swapNumber_ (uint256): The swap number of the bid/closed buy order to complete
  • sellSwapNumber_ (uint256): The swap number of the caller's own open sell offer this settlement draws from, deducting the settled amount from that offer in the same transaction (emitting OrderResized); pass 0 for a standalone settlement that does not draw from an open offer

Requirements:

  • Contract must not be paused
  • Swap must not be canceled or already completed
  • Caller must be the restricted token sender for this swap
  • Swap status must be BuyConfigured
  • Swap must be a closed buy order (open buy orders must be filled via takeOpenBuy, else RestrictedSwap_OpenOrderRequiresTake)
  • Quote token sender must have sufficient quote token balance
  • Restricted token sender must have sufficient restricted token balance
  • Transfer restrictions must be satisfied between parties
  • When sellSwapNumber_ != 0: it must reference an open sell offer owned by the caller, and the settled amount must not exceed that offer's remaining size (RestrictedSwap_ExceedsParentRemaining)

Emits: SwapComplete(swapNumber, restrictedTokenSender, restrictedTokenAmount, quoteTokenSender, quoteToken, quoteTokenAmount, deadline); also OrderResized on the parent offer when sellSwapNumber_ != 0


completeSwapWithRestrictedTokenPermit(uint256 swapNumber_, uint256 sellSwapNumber_, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)

Same as completeSwapWithRestrictedToken, but approves the restricted-token allowance via an EIP-2612 permit signature so approve + settle land in a single transaction.

Parameters:

  • swapNumber_ (uint256): The swap number of the bid/closed buy order to complete
  • sellSwapNumber_ (uint256): The caller's own open sell offer to deduct from, or 0 for a standalone settlement
  • permitValue (uint256): The EIP-2612 value the caller signed. Forwarded to permit verbatim and never recomputed on-chain — see Sizing permitValue
  • permitDeadline (uint256): EIP-2612 permit deadline (must be >= block.timestamp)
  • v, r, s: Components of the permit signature

Requirements:

  • Same as completeSwapWithRestrictedToken
  • The restricted token must implement EIP-2612
  • permitValue sizing (permit overwrites the allowance): with sellSwapNumber_ == 0 it must cover requiredAllowance(caller, restrictedToken) + restrictedTokenAmount; with a parent offer named the delta is zero, so requiredAllowance(caller, restrictedToken) suffices (the parent's reservation already covers the bid). Both measured as of execution time. The post-permit allowance is rechecked, reverting RestrictedSwap_InsufficientRestrictedTokenAllowanceAfterPermit if short

Emits: SwapComplete(...); also OrderResized on the parent offer when sellSwapNumber_ != 0


takeOpenSell(uint256 swapNumber_, uint256 fillAmount)

Fills (fully or partially) an open sell order configured with quoteTokenSender == address(0). The caller receives fillAmount restricted tokens and pays the exact pro-rata quoteTokenAmount * fillAmount / restrictedTokenAmount.

Parameters:

  • swapNumber_ (uint256): The open sell order to fill
  • fillAmount (uint256): Amount of restricted tokens to receive

Requirements:

  • Contract must not be paused
  • Swap must not be canceled or already completed
  • Swap must be an open sell order, else RestrictedSwap_NotOpenOrder
  • Caller must not be the order's restrictedTokenSender, else RestrictedSwap_SelfFillNotAllowed
  • fillAmount must be > 0 and <= remainingRestrictedTokenAmount, else RestrictedSwap_InvalidFillAmount
  • fillAmount must be >= minimumFillAmount unless it takes the entire remaining amount, else RestrictedSwap_FillBelowMinimum
  • The pro-rata quote charge must be a whole number, else RestrictedSwap_AmountNotDivisible
  • Caller must have sufficient quote token balance and allowance
  • Transfer restrictions must be satisfied between parties (checked against the actual taker)

Emits: OpenSwapFilled(swapNumber, filler, fillRestrictedAmount, fillQuoteAmount, remainingRestrictedAmount); also SwapComplete(...) when the fill exhausts the order


takeOpenSellWithPermit(uint256 swapNumber_, uint256 fillAmount, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)

Same as takeOpenSell, but approves the quote-token allowance via an EIP-2612 permit signature.

Parameters:

  • swapNumber_ (uint256): The open sell order to fill
  • fillAmount (uint256): Amount of restricted tokens to receive
  • permitValue (uint256): The EIP-2612 value the caller signed. Forwarded to permit verbatim and never recomputed on-chain — see Sizing permitValue
  • permitDeadline (uint256): EIP-2612 permit deadline (must be >= block.timestamp)
  • v, r, s: Components of the permit signature

Requirements:

  • Same as takeOpenSell
  • The quote token must implement EIP-2612
  • permitValue must cover requiredAllowance(caller, quoteToken) + fillQuoteAmount as of execution time (permit overwrites the allowance); rechecked afterwards, reverting RestrictedSwap_InsufficientQuoteTokenAllowanceAfterPermit if short

Emits: OpenSwapFilled(...); also SwapComplete(...) when the fill exhausts the order


takeOpenBuy(uint256 swapNumber_, uint256 fillAmount, uint256 sellSwapNumber_)

Fills (fully or partially) an open buy order configured with restrictedTokenSender == address(0). The caller delivers fillAmount restricted tokens and receives the exact pro-rata quote amount.

Parameters:

  • swapNumber_ (uint256): The open buy order to fill
  • fillAmount (uint256): Amount of restricted tokens to deliver
  • sellSwapNumber_ (uint256): The caller's own open sell offer this delivery draws from, deducting fillAmount from that offer in the same transaction (emitting OrderResized); pass 0 when the fill draws from no open offer

Requirements:

  • Contract must not be paused
  • Swap must not be canceled or already completed
  • Swap must be an open buy order, else RestrictedSwap_NotOpenOrder
  • Caller must not be the order's quoteTokenSender, else RestrictedSwap_SelfFillNotAllowed
  • fillAmount must be > 0 and <= remainingRestrictedTokenAmount, else RestrictedSwap_InvalidFillAmount
  • fillAmount must be >= minimumFillAmount unless it takes the entire remaining amount, else RestrictedSwap_FillBelowMinimum
  • The pro-rata quote credit must be a whole number, else RestrictedSwap_AmountNotDivisible
  • Caller must have sufficient restricted token balance and allowance
  • Transfer restrictions must be satisfied between parties (checked against the actual taker)
  • When sellSwapNumber_ != 0: it must reference an open sell offer owned by the caller, fillAmount must divide cleanly against that offer's price ratio, and must not exceed its remaining size (RestrictedSwap_ExceedsParentRemaining)

Emits: OpenSwapFilled(...); also SwapComplete(...) when the fill exhausts the order, and OrderResized on the parent offer when sellSwapNumber_ != 0


takeOpenBuyWithPermit(uint256 swapNumber_, uint256 fillAmount, uint256 sellSwapNumber_, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)

Same as takeOpenBuy, but approves the restricted-token allowance via an EIP-2612 permit signature.

Parameters:

  • swapNumber_ (uint256): The open buy order to fill
  • fillAmount (uint256): Amount of restricted tokens to deliver
  • sellSwapNumber_ (uint256): The caller's own open sell offer to deduct from, or 0
  • permitValue (uint256): The EIP-2612 value the caller signed. Forwarded to permit verbatim and never recomputed on-chain — see Sizing permitValue
  • permitDeadline (uint256): EIP-2612 permit deadline (must be >= block.timestamp)
  • v, r, s: Components of the permit signature

Requirements:

  • Same as takeOpenBuy
  • The restricted token must implement EIP-2612
  • permitValue sizing (permit overwrites the allowance): with sellSwapNumber_ == 0 sign requiredAllowance(caller, restrictedToken) + fillAmount; with a parent offer named the delta is zero, so requiredAllowance(caller, restrictedToken) suffices. Both measured as of execution time. Rechecked afterwards, reverting RestrictedSwap_InsufficientRestrictedTokenAllowanceAfterPermit if short

Emits: OpenSwapFilled(...); also SwapComplete(...) when the fill exhausts the order, and OrderResized on the parent offer when sellSwapNumber_ != 0


cancelSwap(uint256 swapNumber_)

Cancels a configured swap. For non-expired swaps, only the original parties can cancel. For expired swaps, anyone can cancel.

Parameters:

  • swapNumber_ (uint256): The swap number to cancel

Requirements:

  • Swap must not be canceled or already completed
  • Swap must be properly configured
  • For non-expired swaps:
    • For SellConfigured swaps: caller must be the restricted token sender
    • For BuyConfigured swaps: caller must be the quote token sender
  • For expired swaps: anyone can cancel

Emits: SwapCanceled(sender, swapNumber)


Order Management Functions

decreaseOrder(uint256 swapNumber, uint256 newRemainingRestrictedAmount)

Shrinks an active open order's remaining size in place (owner-only, decrease-only) — the single-signature alternative to cancel-and-recreate. The paired quote amount shrinks exactly pro-rata on the order's original price. Shrinking to 0 cancels the order. Like cancelSwap, callable while paused and on expired orders (exit-type action).

Parameters:

  • swapNumber (uint256): The caller's open order
  • newRemainingRestrictedAmount (uint256): New remaining restricted-token size; must be strictly less than the current remaining amount (0 cancels the order)

Requirements:

  • Caller must be the order's creator, else RestrictedSwap_InvalidOrderOwner
  • Swap must be an open order, else RestrictedSwap_NotOpenOrder
  • New size must be strictly smaller than the current remaining amount, else RestrictedSwap_InvalidResizeAmount
  • The new quote remainder must be a whole number at the original price ratio, else RestrictedSwap_AmountNotDivisible

Emits: OrderResized(swapNumber, newRemainingRestrictedAmount, newRemainingQuoteAmount); also SwapCanceled(...) when shrunk to zero


increaseOrder(uint256 swapNumber, uint256 newRemainingRestrictedAmount)

Grows an active open order's remaining size in place (owner-only, increase-only). Because growing creates new exposure, this carries configure-grade guards: blocked while paused, blocked on expired orders, and the creator's allowance (and, for buys, quote balance) must already cover all reservations plus the growth. After an increase the remaining size may exceed the originally configured amount — derive traded totals from OpenSwapFilled events, not configured amounts.

Parameters:

  • swapNumber (uint256): The caller's open order
  • newRemainingRestrictedAmount (uint256): New remaining restricted-token size; must be strictly greater than the current remaining amount

Requirements:

  • Caller must be the order's creator, else RestrictedSwap_InvalidOrderOwner
  • Contract must not be paused; order must not be expired
  • Swap must be an open order, else RestrictedSwap_NotOpenOrder
  • New size must be strictly greater than the current remaining amount, else RestrictedSwap_InvalidResizeAmount
  • The added amount must divide cleanly against the order's original price ratio, else RestrictedSwap_AmountNotDivisible
  • The creator's allowance must cover all reservations plus the growth (open buys also re-check quote balance against the order's new total)

Emits: OrderResized(swapNumber, newRemainingRestrictedAmount, newRemainingQuoteAmount)


increaseOrderWithPermit(uint256 swapNumber, uint256 newRemainingRestrictedAmount, uint256 permitValue, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)

Same as increaseOrder, but tops up the allowance via an EIP-2612 permit signature so the top-up and the size increase land atomically. The permitted token is the restricted token for an open sell, or the order's quote token for an open buy.

Parameters:

  • swapNumber (uint256): The caller's open order
  • newRemainingRestrictedAmount (uint256): New remaining restricted-token size (strictly greater than current)
  • permitValue (uint256): The EIP-2612 value the caller signed. Forwarded to permit verbatim and never recomputed on-chain — see Sizing permitValue
  • permitDeadline (uint256): EIP-2612 permit deadline (must be >= block.timestamp)
  • v, r, s: Components of the permit signature

Requirements:

  • Same as increaseOrder
  • The permitted token must implement EIP-2612
  • permitValue must cover requiredAllowance + added (open sell, restricted token) or requiredAllowance + quoteAdded (open buy, quote token) as of execution time, since permit overwrites the allowance. Only one side's permit is consumed, so a single signed value covers either branch

Emits: OrderResized(swapNumber, newRemainingRestrictedAmount, newRemainingQuoteAmount)


Query Functions

swapNumber() → uint256

Returns the current swap number counter.

Returns:

  • uint256: The current swap number (total swaps configured)

swapStatus(uint256 swapNumber_) → SwapStatus

Returns the status of a specific swap.

Parameters:

  • swapNumber_ (uint256): The swap number to query

Returns:

  • SwapStatus: The status enum (SellConfigured=1, BuyConfigured=2, Complete=3, Canceled=4)

Requirements:

  • Swap record must exist

remainingRestrictedTokenAmount(uint256 swapNumber_) → uint256

Returns the restricted-token amount that was never filled on a swap. Only open orders decrement it (each partial fill, plus decreaseOrder / increaseOrder resizes); a closed swap settles its original amounts in full, so it reports its configure-time amount for its whole lifetime.

Terminal states:

  • Complete → always 0.
  • Canceledretains the amount withdrawn unfilled, deliberately kept as on-chain history of what was pulled back rather than traded (the refund itself is visible in pendingSells / requiredAllowance dropping). The one exception is an order retired by shrinking to zero — via decreaseOrder(swapNumber, 0) or a parent-offer deduction — where the shrink already consumed the remainder, so it reports 0.

Read swapStatus(swapNumber) alongside this getter: a non-zero value on a terminal record is history, not available inventory.

Parameters:

  • swapNumber_ (uint256): The swap number to query

Returns:

  • uint256: The unfilled restricted-token amount

minimumFillAmount(uint256 swapNumber_) → uint256

Returns the minimum restricted-token amount a taker must fill on an open order (unless taking the entire remaining amount).

Parameters:

  • swapNumber_ (uint256): The swap number to query

Returns:

  • uint256: The configured minimum fill amount (0 means no minimum)

isOpenOrder(uint256 swapNumber_) → bool

Returns whether a swap is an open order (one zero counterparty) that must be filled via takeOpenSell / takeOpenBuy.

Parameters:

  • swapNumber_ (uint256): The swap number to query

Returns:

  • bool: True if the swap is an open order

maxSwapLifetime() → uint256

Returns the maximum allowed distance between block.timestamp and a swap's deadline at configure time, fixed at deployment.

Returns:

  • uint256: The maximum swap lifetime in seconds

MIN_SWAP_LIFETIME_LIMIT() → uint256

Returns the compile-time floor that maxSwapLifetime must not fall below at deployment.

Returns:

  • uint256: The minimum allowed value for maxSwapLifetime

MAX_SWAP_LIFETIME_LIMIT() → uint256

Returns the compile-time ceiling that maxSwapLifetime must not exceed at deployment.

Returns:

  • uint256: The maximum allowed value for maxSwapLifetime

requiredAllowance(address addr, address token) → uint256

Returns the required allowance for active swaps for a wallet and token.

Parameters:

  • addr (address): Wallet address
  • token (address): Token address

Returns:

  • uint256: Required allowance amount for the configurator

requiredAllowanceCounterparty(address addr, address token) → uint256

Returns the required allowance for active swaps as a counterparty.

Parameters:

  • addr (address): Wallet address
  • token (address): Token address

Returns:

  • uint256: Required allowance amount as counterparty (informational only)

pendingBuys(address addr) → uint256

Returns pending restricted tokens to buy for an address.

Parameters:

  • addr (address): Buyer address

Returns:

  • uint256: Amount of pending restricted tokens to buy

pendingSells(address addr) → uint256

Returns pending restricted tokens to sell for an address.

Parameters:

  • addr (address): Seller address

Returns:

  • uint256: Amount of pending restricted tokens to sell

restrictedLockupToken() → address

Returns the RestrictedLockupToken contract address.

Returns:

  • address: The RestrictedLockupToken contract address

accessControl() → address

Returns the AccessControl contract address.

Returns:

  • address: The AccessControl contract address

INTERFACE_ID() → bytes4

Returns the interface ID for ERC-165 support.

Returns:

  • bytes4: The interface identifier

isSwapExpired(uint256 swapNumber_) → bool

Checks if a specific swap has expired based on its deadline.

Parameters:

  • swapNumber_ (uint256): The swap number to check

Returns:

  • bool: True if the swap has expired, false otherwise

Requirements:

  • Swap record must exist

Note:

  • A swap is considered expired if it has a deadline > 0 and the current block timestamp is greater than the deadline
  • Configured swaps always carry a deadline > 0 (the 0 sentinel is rejected at configure time)

Inherited Functions

Pausable Functions

paused() → bool

Returns whether the contract is currently paused.

Returns:

  • bool: True if paused, false otherwise

ERC-2771 Functions

isTrustedForwarder(address forwarder) → bool

Checks if an address is a trusted forwarder for meta-transactions.

Parameters:

  • forwarder (address): Address to check

Returns:

  • bool: True if the address is a trusted forwarder

trustedForwarder() → address

Returns the trusted forwarder address.

Returns:

  • address: The trusted forwarder address

ERC-165 Functions

supportsInterface(bytes4 interfaceId) → bool

Checks if the contract supports a specific interface.

Parameters:

  • interfaceId (bytes4): Interface identifier to check

Returns:

  • bool: True if the interface is supported

Events

SwapConfigured(uint256 indexed swapNumber, address indexed restrictedTokenSender, uint256 restrictedTokenAmount, address quoteToken, address indexed quoteTokenSender, uint256 quoteTokenAmount, uint256 deadline, uint256 minimumFillAmount)

Emitted when a new swap is configured. An open sell order carries quoteTokenSender == address(0); an open buy order carries restrictedTokenSender == address(0).

Parameters:

  • swapNumber (uint256, indexed): Unique swap identifier
  • restrictedTokenSender (address, indexed): Address providing restricted tokens (address(0) for an open buy order)
  • restrictedTokenAmount (uint256): Amount of restricted tokens in the swap
  • quoteToken (address): Quote token contract address
  • quoteTokenSender (address, indexed): Address providing quote tokens (address(0) for an open sell order)
  • quoteTokenAmount (uint256): Amount of quote tokens in the swap
  • deadline (uint256): Unix timestamp deadline for swap expiration
  • minimumFillAmount (uint256): Minimum restricted-token amount a taker must fill on an open order (unless taking the entire remaining amount); 0 means no minimum

SwapComplete(uint256 indexed swapNumber, address indexed restrictedTokenSender, uint256 restrictedTokenAmount, address indexed quoteTokenSender, address quoteToken, uint256 quoteTokenAmount, uint256 deadline)

Emitted when a swap is successfully completed.

Parameters:

  • swapNumber (uint256, indexed): Unique swap identifier
  • restrictedTokenSender (address, indexed): Address that provided restricted tokens
  • restrictedTokenAmount (uint256): Amount of restricted tokens swapped
  • quoteTokenSender (address, indexed): Address that provided quote tokens
  • quoteToken (address): Quote token contract address
  • quoteTokenAmount (uint256): Amount of quote tokens swapped
  • deadline (uint256): Unix timestamp deadline for swap expiration

SwapCanceled(address indexed sender, uint256 indexed swapNumber)

Emitted when a swap is canceled.

Parameters:

  • sender (address, indexed): Address that canceled the swap
  • swapNumber (uint256, indexed): Unique swap identifier

OpenSwapFilled(uint256 indexed swapNumber, address indexed filler, uint256 fillRestrictedAmount, uint256 fillQuoteAmount, uint256 remainingRestrictedAmount)

Emitted on every partial or final fill of an open order via takeOpenSell / takeOpenBuy. A remainingRestrictedAmount of 0 indicates the order is now fully filled (a SwapComplete is emitted in the same transaction with the last filler as counterparty).

Parameters:

  • swapNumber (uint256, indexed): Unique swap identifier
  • filler (address, indexed): Address that took this fill (the actual counterparty for this slice)
  • fillRestrictedAmount (uint256): Restricted-token amount transferred in this fill
  • fillQuoteAmount (uint256): Quote-token amount transferred in this fill
  • remainingRestrictedAmount (uint256): Restricted-token amount still unfilled after this event

OrderResized(uint256 indexed swapNumber, uint256 newRemainingRestrictedAmount, uint256 newRemainingQuoteAmount)

Emitted when an open order's remaining size changes without a fill — the owner called decreaseOrder / increaseOrder / increaseOrderWithPermit, or a linked settlement deducted from a parent offer (completeSwapWithRestrictedToken / takeOpenBuy with a sellSwapNumber). The size can both shrink and grow (possibly above the originally configured amount). If a shrink reaches zero, SwapCanceled is emitted in the same transaction and the order is terminal.

Parameters:

  • swapNumber (uint256, indexed): Unique swap identifier of the resized order
  • newRemainingRestrictedAmount (uint256): Restricted-token amount still offered after the resize
  • newRemainingQuoteAmount (uint256): Quote-token amount still expected after the resize

Inherited Events

Paused(address account)

Emitted when the contract is paused.

Parameters:

  • account (address): Address that paused the contract

Unpaused(address account)

Emitted when the contract is unpaused.

Parameters:

  • account (address): Address that unpaused the contract

Custom Errors

Configuration Errors

RestrictedSwap_InvalidAccessControl()

Thrown when an invalid access control address is provided during construction.


RestrictedSwap_InvalidRestrictedLockupToken()

Thrown when a zero address is provided as the RestrictedLockupToken during construction.


RestrictedSwap_InvalidTrustedForwarder()

Thrown when a zero address is provided as the trusted forwarder during construction.


RestrictedSwap_InvalidMaxSwapLifetime()

Thrown when the constructor's maxSwapLifetime_ is outside [MIN_SWAP_LIFETIME_LIMIT, MAX_SWAP_LIFETIME_LIMIT].


RestrictedSwap_InvalidQuoteToken()

Thrown when a zero address is provided as the quote token.


RestrictedSwap_InvalidQuoteTokenSender()

Thrown when a zero address is provided as the quote token sender.


RestrictedSwap_InvalidRestrictedTokenSender()

Thrown when a zero address is provided as the restricted token sender.


RestrictedSwap_InvalidRestrictedTokenAmount()

Thrown when zero is provided as the restricted token amount.


RestrictedSwap_InvalidQuoteTokenAmount()

Thrown when zero is provided as the quote token amount.


Validation Errors

RestrictedSwap_QuoteTokenMustNotSupportIERC1404()

Thrown when the quote token supports ERC-1404 interface (restricted tokens not allowed as quote tokens).


RestrictedSwap_InsufficientRestrictedTokenAllowance()

Thrown when the restricted token allowance is insufficient for the swap.


RestrictedSwap_InsufficientQuoteTokenAllowance()

Thrown when the quote token allowance is insufficient for the swap.


RestrictedSwap_InsufficientRestrictedTokenAmount()

Thrown when the restricted token balance is insufficient for the swap.


RestrictedSwap_InsufficientQuoteTokenAmount()

Thrown when the quote token balance is insufficient for the swap.


RestrictedSwap_InsufficientRestrictedTokenAllowanceAfterPermit()

Thrown by a ...WithPermit function when the restricted-token allowance is still insufficient after applying the permit signature (e.g. permitValue was under-sized for the requirement at execution time, or the signature was invalid).


RestrictedSwap_InsufficientQuoteTokenAllowanceAfterPermit()

Thrown by a ...WithPermit function when the quote-token allowance is still insufficient after applying the permit signature (e.g. permitValue was under-sized for the requirement at execution time, or the signature was invalid).


RestrictedSwap_DeadlineRequired()

Thrown at configure time when deadline is 0 (the "no deadline" sentinel is no longer accepted).


RestrictedSwap_DeadlineExceedsMaxLifetime()

Thrown at configure time when deadline - block.timestamp exceeds the contract's maxSwapLifetime.


Swap State Errors

RestrictedSwap_AlreadyCanceled()

Thrown when attempting to operate on a swap that has already been canceled.


RestrictedSwap_AlreadyCompleted()

Thrown when attempting to operate on a swap that has already been completed.


RestrictedSwap_InvalidSwapStatus()

Thrown when the swap status doesn't match the required status for the operation.


RestrictedSwap_InvalidSwapRecord()

Thrown when attempting to query a swap that doesn't exist.


RestrictedSwap_SwapExpired()

Thrown when attempting to complete a swap that has expired based on its deadline.


RestrictedSwap_SwapNotConfigured()

Thrown when attempting to cancel a swap that hasn't been properly configured.


Authorization Errors

RestrictedSwap_InvalidTokenSender()

Thrown when the caller is not authorized to complete the swap (not the expected token sender).


RestrictedSwap_InvalidCanceler()

Thrown when the caller is not authorized to cancel the swap via cancelSwap (only the owner may cancel a live order; anyone may cancel an expired one). Owner checks on the resize paths use RestrictedSwap_InvalidOrderOwner instead.


RestrictedSwap_InvalidOrderOwner()

Thrown when decreaseOrder / increaseOrder is called by someone other than the order's creator.


Open Order & Resize Errors

RestrictedSwap_NotOpenOrder()

Thrown when an open-order-only operation (takeOpenSell / takeOpenBuy / decreaseOrder / increaseOrder) targets a closed swap (both counterparties named).


RestrictedSwap_OpenOrderRequiresTake()

Thrown when completeSwapWithQuoteToken / completeSwapWithRestrictedToken (or their permit variants) target an open order, which must instead be filled via takeOpenSell / takeOpenBuy.


RestrictedSwap_SelfFillNotAllowed()

Thrown when the configurer of an open order attempts to fill it themselves (takeOpenSell by the order's restrictedTokenSender, or takeOpenBuy by its quoteTokenSender).


RestrictedSwap_InvalidFillAmount()

Thrown when a takeOpenSell / takeOpenBuy fill amount is zero or exceeds the order's remaining size.


RestrictedSwap_FillBelowMinimum()

Thrown when a take fill is below the order's minimumFillAmount and does not take the entire remaining amount (a below-minimum tail can always be taken in full).


RestrictedSwap_InvalidMinimumFillAmount()

Thrown at configure time when minimumFillAmount exceeds restrictedTokenAmount, or is non-zero on a closed swap (both counterparties named — closed swaps settle in full and never consult the minimum).


RestrictedSwap_AmountNotDivisible()

Thrown when a fill, resize, or linked parent deduction amount does not produce an integer quote amount at the order's original price ratio (strict exact-pricing rule).


RestrictedSwap_InvalidResizeAmount()

Thrown when decreaseOrder is given a size that is not strictly smaller, or increaseOrder a size that is not strictly larger, than the order's current remaining amount.


RestrictedSwap_ExceedsParentRemaining()

Thrown when a linked settle or fill (sellSwapNumber named) would deduct more than the parent offer's remaining size — e.g. a third party filled the parent between signing and execution. Callers should retry with sellSwapNumber = 0 and resize the listing separately.


Transfer Errors

RestrictedSwap_InconsistentQuoteTokenAmount()

Thrown when the actual quote token transfer amount doesn't match the expected amount.


RestrictedSwap_InconsistentRestrictedTokenAmount()

Thrown when the actual restricted token transfer amount doesn't match the expected amount.


Inherited Errors

EasyAccessControl_DoesNotHaveContractOrTransferAdminRole(address addr)

Thrown when an address lacks both CONTRACT_ADMIN_ROLE and TRANSFER_ADMIN_ROLE.

Parameters:

  • addr (address): The unauthorized address

EnforcedPause()

Thrown when an operation is attempted while the contract is paused.


ExpectedPause()

Thrown when attempting to pause an already paused contract or unpause an unpaused contract.


ReentrancyGuardReentrantCall()

Thrown when a reentrant call is detected.


SafeERC20FailedOperation(address token)

Thrown when an ERC-20 operation fails.

Parameters:

  • token (address): The token contract address where the operation failed